cleanup unused references.
simplify authorization url genreation using fragment oauth mode to support stateless Lighthouse.
This commit is contained in:
parent
9f6e32119f
commit
f03bdbd122
|
@ -20,6 +20,7 @@ const routes: Routes = [
|
||||||
{ path: 'source/:source_id', component: SourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
|
{ path: 'source/:source_id', component: SourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
|
||||||
{ path: 'source/:source_id/resource/:resource_id', component: ResourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
|
{ path: 'source/:source_id/resource/:resource_id', component: ResourceDetailComponent, canActivate: [ CanActivateAuthGuard] },
|
||||||
{ path: 'sources', component: MedicalSourcesComponent, canActivate: [ CanActivateAuthGuard] },
|
{ path: 'sources', component: MedicalSourcesComponent, canActivate: [ CanActivateAuthGuard] },
|
||||||
|
{ path: 'sources/callback/:source_type', component: MedicalSourcesComponent, canActivate: [ CanActivateAuthGuard] },
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
export class LighthouseSourceMetadata {
|
||||||
|
authorization_endpoint: string
|
||||||
|
token_endpoint: string
|
||||||
|
introspection_endpoint: string
|
||||||
|
userinfo_endpoint: string
|
||||||
|
|
||||||
|
scopes_supported: string[]
|
||||||
|
issuer: string
|
||||||
|
grant_types_supported: string[]
|
||||||
|
response_types_supported: string[]
|
||||||
|
aud: string
|
||||||
|
code_challenge_methods_supported: string[]
|
||||||
|
|
||||||
|
api_endpoint_base_url: string
|
||||||
|
client_id: string
|
||||||
|
redirect_uri: string
|
||||||
|
|
||||||
|
confidential: boolean
|
||||||
|
}
|
|
@ -1,17 +0,0 @@
|
||||||
export class LighthouseSource {
|
|
||||||
oauth_authorization_endpoint: string
|
|
||||||
oauth_token_endpoint: string
|
|
||||||
oauth_registration_endpoint: string
|
|
||||||
oauth_introspection_endpoint: string
|
|
||||||
oauth_userinfo_endpoint: string
|
|
||||||
oauth_token_endpoint_auth_methods_supported: string
|
|
||||||
|
|
||||||
api_endpoint_base_url: string
|
|
||||||
response_type: string
|
|
||||||
client_id: string
|
|
||||||
scopes: string[]
|
|
||||||
redirect_uri: string
|
|
||||||
aud: string
|
|
||||||
|
|
||||||
confidential: boolean
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { LighthouseSource } from './lighthouse-source';
|
|
||||||
|
|
||||||
describe('LighthouseSource', () => {
|
|
||||||
it('should create an instance', () => {
|
|
||||||
expect(new LighthouseSource()).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import {FastenApiService} from '../../services/fasten-api.service';
|
import {FastenApiService} from '../../services/fasten-api.service';
|
||||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
|
||||||
import {User} from '../../models/fasten/user';
|
import {User} from '../../models/fasten/user';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import {FastenApiService} from '../../services/fasten-api.service';
|
import {FastenApiService} from '../../services/fasten-api.service';
|
||||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
|
||||||
import {Source} from '../../models/fasten/source';
|
import {Source} from '../../models/fasten/source';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {Summary} from '../../models/fasten/summary';
|
import {Summary} from '../../models/fasten/summary';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {Component, HostListener, OnInit} from '@angular/core';
|
import {Component, HostListener, OnInit} from '@angular/core';
|
||||||
import {LighthouseService} from '../../services/lighthouse.service';
|
import {LighthouseService} from '../../services/lighthouse.service';
|
||||||
import {FastenApiService} from '../../services/fasten-api.service';
|
import {FastenApiService} from '../../services/fasten-api.service';
|
||||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
import {LighthouseSourceMetadata} from '../../models/lighthouse/lighthouse-source-metadata';
|
||||||
import * as Oauth from '@panva/oauth4webapi';
|
import * as Oauth from '@panva/oauth4webapi';
|
||||||
import {AuthorizeClaim} from '../../models/lighthouse/authorize-claim';
|
import {AuthorizeClaim} from '../../models/lighthouse/authorize-claim';
|
||||||
import {Source} from '../../models/fasten/source';
|
import {Source} from '../../models/fasten/source';
|
||||||
|
@ -12,6 +12,9 @@ import {concatMap, delay, retryWhen, timeout, first, map, filter, catchError} fr
|
||||||
import * as FHIR from "fhirclient"
|
import * as FHIR from "fhirclient"
|
||||||
import {MetadataSource} from '../../models/fasten/metadata-source';
|
import {MetadataSource} from '../../models/fasten/metadata-source';
|
||||||
import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap';
|
import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {ActivatedRoute, Router} from '@angular/router';
|
||||||
|
import {Location} from '@angular/common';
|
||||||
|
// If you dont import this angular will import the wrong "Location"
|
||||||
|
|
||||||
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
export const sourceConnectWindowTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
|
||||||
|
|
||||||
|
@ -26,6 +29,9 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
private lighthouseApi: LighthouseService,
|
private lighthouseApi: LighthouseService,
|
||||||
private fastenApi: FastenApiService,
|
private fastenApi: FastenApiService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private location: Location,
|
||||||
) { }
|
) { }
|
||||||
status: { [name: string]: string } = {}
|
status: { [name: string]: string } = {}
|
||||||
|
|
||||||
|
@ -43,6 +49,11 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
this.fastenApi.getMetadataSources().subscribe((metadataSources: {[name:string]: MetadataSource}) => {
|
this.fastenApi.getMetadataSources().subscribe((metadataSources: {[name:string]: MetadataSource}) => {
|
||||||
this.metadataSources = metadataSources
|
this.metadataSources = metadataSources
|
||||||
|
|
||||||
|
const callbackSourceType = this.route.snapshot.paramMap.get('source_type')
|
||||||
|
if(callbackSourceType){
|
||||||
|
this.callback(callbackSourceType).then(console.log)
|
||||||
|
}
|
||||||
|
|
||||||
this.fastenApi.getSources()
|
this.fastenApi.getSources()
|
||||||
.subscribe((sourceList: Source[]) => {
|
.subscribe((sourceList: Source[]) => {
|
||||||
|
|
||||||
|
@ -73,112 +84,109 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
this.status[sourceType] = "authorize"
|
this.status[sourceType] = "authorize"
|
||||||
|
|
||||||
this.lighthouseApi.getLighthouseSource(sourceType)
|
this.lighthouseApi.getLighthouseSource(sourceType)
|
||||||
.subscribe(async (connectData: LighthouseSource) => {
|
.subscribe(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||||
console.log(connectData);
|
console.log(sourceMetadata);
|
||||||
|
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata)
|
||||||
const state = this.uuidV4()
|
|
||||||
|
|
||||||
let authorizationUrl
|
|
||||||
|
|
||||||
//only set if this is not a "confidential" source.
|
|
||||||
let codeVerifier
|
|
||||||
let codeChallenge
|
|
||||||
let codeChallengeMethod
|
|
||||||
|
|
||||||
if(connectData.confidential){
|
|
||||||
authorizationUrl = this.lighthouseApi.generateConfidentialSourceAuthorizeUrl(state, connectData)
|
|
||||||
} else {
|
|
||||||
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
|
||||||
codeVerifier = Oauth.generateRandomCodeVerifier();
|
|
||||||
codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
|
||||||
codeChallengeMethod = 'S256';
|
|
||||||
|
|
||||||
authorizationUrl = this.lighthouseApi.generatePKCESourceAuthorizeUrl(codeVerifier, codeChallenge, codeChallengeMethod, state, connectData)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('authorize url:', authorizationUrl.toString());
|
console.log('authorize url:', authorizationUrl.toString());
|
||||||
// open new browser window
|
// redirect to lighthouse with uri's
|
||||||
let openedWindow = window.open(authorizationUrl.toString(), "_blank");
|
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType)
|
||||||
|
|
||||||
//wait for response
|
|
||||||
// TODO: throw an error if we timeout or an error occurs during authentication.
|
|
||||||
this.waitForClaimOrTimeout(openedWindow, sourceType, state).subscribe(async (claimData: AuthorizeClaim) => {
|
|
||||||
console.log("claim response:", claimData)
|
|
||||||
this.status[sourceType] = "token"
|
|
||||||
|
|
||||||
let payload: any
|
|
||||||
if(connectData.confidential){
|
|
||||||
|
|
||||||
// we should have an access_token (and optionally a refresh_token) in the claim
|
|
||||||
payload = claimData
|
|
||||||
|
|
||||||
//patient may be returned as patient_id
|
|
||||||
payload.patient = payload.patient ? payload.patient : payload.patient_id
|
|
||||||
|
|
||||||
} else {
|
|
||||||
payload = await this.swapOauthPKCEToken(state, codeVerifier, authorizationUrl, connectData, claimData)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//If payload.patient is not set, make sure we extract the patient ID from the id_token or make an introspection req
|
|
||||||
if(!payload.patient && payload.id_token){
|
|
||||||
//
|
|
||||||
console.log("NO PATIENT ID present, decoding jwt to extract patient")
|
|
||||||
//const introspectionResp = await Oauth.introspectionRequest(as, client, payload.access_token)
|
|
||||||
//console.log(introspectionResp)
|
|
||||||
payload.patient = jwtDecode(payload.id_token, new BrowserAdapter())["profile"].replace(/^(Patient\/)/,'')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Create FHIR Client
|
|
||||||
|
|
||||||
const sourceCredential: Source = {
|
|
||||||
source_type: sourceType,
|
|
||||||
oauth_authorization_endpoint: connectData.oauth_authorization_endpoint,
|
|
||||||
oauth_token_endpoint: connectData.oauth_token_endpoint,
|
|
||||||
oauth_registration_endpoint: connectData.oauth_registration_endpoint,
|
|
||||||
oauth_introspection_endpoint: connectData.oauth_introspection_endpoint,
|
|
||||||
oauth_userinfo_endpoint: connectData.oauth_userinfo_endpoint,
|
|
||||||
oauth_token_endpoint_auth_methods_supported: connectData.oauth_token_endpoint_auth_methods_supported,
|
|
||||||
api_endpoint_base_url: connectData.api_endpoint_base_url,
|
|
||||||
client_id: connectData.client_id,
|
|
||||||
redirect_uri: connectData.redirect_uri,
|
|
||||||
scopes: connectData.scopes ? connectData.scopes.join(' ') : undefined,
|
|
||||||
patient_id: payload.patient,
|
|
||||||
access_token: payload.access_token,
|
|
||||||
refresh_token: payload.refresh_token,
|
|
||||||
id_token: payload.id_token,
|
|
||||||
code_challenge: codeChallenge,
|
|
||||||
code_verifier: codeVerifier,
|
|
||||||
|
|
||||||
// @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db.
|
|
||||||
expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())),
|
|
||||||
confidential: connectData.confidential
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.fastenApi.createSource(sourceCredential).subscribe(
|
|
||||||
(respData) => {
|
|
||||||
delete this.status[sourceType]
|
|
||||||
window.location.reload();
|
|
||||||
|
|
||||||
console.log("source credential create response:", respData)
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
delete this.status[sourceType]
|
|
||||||
window.location.reload();
|
|
||||||
|
|
||||||
console.log(err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async callback(sourceType: string) {
|
||||||
|
|
||||||
|
//get the source metadata again
|
||||||
|
await this.lighthouseApi.getLighthouseSource(sourceType)
|
||||||
|
.subscribe(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||||
|
|
||||||
|
//get required parameters from the URI and local storage
|
||||||
|
const callbackUrlParts = new URL(window.location.href)
|
||||||
|
const fragmentParams = new URLSearchParams(callbackUrlParts.hash.substring(1))
|
||||||
|
const callbackCode = callbackUrlParts.searchParams.get("code") || fragmentParams.get("code")
|
||||||
|
const callbackState = callbackUrlParts.searchParams.get("state") || fragmentParams.get("state")
|
||||||
|
const callbackError = callbackUrlParts.searchParams.get("error") || fragmentParams.get("error")
|
||||||
|
const callbackErrorDescription = callbackUrlParts.searchParams.get("error_description") || fragmentParams.get("error_description")
|
||||||
|
|
||||||
|
//reset the url, removing the params and fragment from the current url.
|
||||||
|
const urlTree = this.router.createUrlTree(["/sources"],{
|
||||||
|
relativeTo: this.route,
|
||||||
|
});
|
||||||
|
this.location.replaceState(urlTree.toString());
|
||||||
|
|
||||||
|
const expectedState = localStorage.getItem(`${sourceType}:state`)
|
||||||
|
localStorage.removeItem(`${sourceType}:state`)
|
||||||
|
|
||||||
|
|
||||||
|
if(callbackError && !callbackCode){
|
||||||
|
//TOOD: print this message in the UI
|
||||||
|
console.error("an error occurred while authenticating to this source. Please try again later", callbackErrorDescription)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("callback code:", callbackCode)
|
||||||
|
this.status[sourceType] = "token"
|
||||||
|
|
||||||
|
let payload: any
|
||||||
|
payload = await this.lighthouseApi.swapOauthToken(sourceType, sourceMetadata,expectedState, callbackState, callbackCode)
|
||||||
|
|
||||||
|
|
||||||
|
//If payload.patient is not set, make sure we extract the patient ID from the id_token or make an introspection req
|
||||||
|
if(!payload.patient && payload.id_token){
|
||||||
|
//
|
||||||
|
console.log("NO PATIENT ID present, decoding jwt to extract patient")
|
||||||
|
//const introspectionResp = await Oauth.introspectionRequest(as, client, payload.access_token)
|
||||||
|
//console.log(introspectionResp)
|
||||||
|
payload.patient = jwtDecode(payload.id_token, new BrowserAdapter())["profile"].replace(/^(Patient\/)/,'')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//Create FHIR Client
|
||||||
|
|
||||||
|
const sourceCredential: Source = {
|
||||||
|
source_type: sourceType,
|
||||||
|
oauth_authorization_endpoint: sourceMetadata.authorization_endpoint,
|
||||||
|
oauth_token_endpoint: sourceMetadata.token_endpoint,
|
||||||
|
oauth_registration_endpoint: "",
|
||||||
|
oauth_introspection_endpoint: sourceMetadata.introspection_endpoint,
|
||||||
|
oauth_userinfo_endpoint: sourceMetadata.userinfo_endpoint,
|
||||||
|
oauth_token_endpoint_auth_methods_supported: "",
|
||||||
|
api_endpoint_base_url: sourceMetadata.api_endpoint_base_url,
|
||||||
|
client_id: sourceMetadata.client_id,
|
||||||
|
redirect_uri: sourceMetadata.redirect_uri,
|
||||||
|
scopes: sourceMetadata.scopes_supported ? sourceMetadata.scopes_supported.join(' ') : undefined,
|
||||||
|
patient_id: payload.patient,
|
||||||
|
access_token: payload.access_token,
|
||||||
|
refresh_token: payload.refresh_token,
|
||||||
|
id_token: payload.id_token,
|
||||||
|
code_challenge: "",
|
||||||
|
code_verifier: "",
|
||||||
|
|
||||||
|
// @ts-ignore - in some cases the getAccessTokenExpiration is a string, which cases failures to store Source in db.
|
||||||
|
expires_at: parseInt(getAccessTokenExpiration(payload, new BrowserAdapter())),
|
||||||
|
confidential: sourceMetadata.confidential
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.fastenApi.createSource(sourceCredential).subscribe(
|
||||||
|
(respData) => {
|
||||||
|
delete this.status[sourceType]
|
||||||
|
// window.location.reload();
|
||||||
|
|
||||||
|
console.log("source credential create response:", respData)
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
delete this.status[sourceType]
|
||||||
|
// window.location.reload();
|
||||||
|
|
||||||
|
console.log(err)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
uploadSourceBundle(event) {
|
uploadSourceBundle(event) {
|
||||||
this.uploadedFile = [event.addedFiles[0]]
|
this.uploadedFile = [event.addedFiles[0]]
|
||||||
this.fastenApi.createManualSource(event.addedFiles[0]).subscribe(
|
this.fastenApi.createManualSource(event.addedFiles[0]).subscribe(
|
||||||
|
@ -229,80 +237,4 @@ export class MedicalSourcesComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private waitForClaimOrTimeout(openedWindow: Window, sourceType: string, state: string): Observable<AuthorizeClaim> {
|
|
||||||
console.log(`waiting for postMessage notification from ${sourceType} window`)
|
|
||||||
|
|
||||||
//new code to listen to post message
|
|
||||||
return fromEvent(window, 'message')
|
|
||||||
.pipe(
|
|
||||||
//throw an error if we wait more than 2 minutes (this will close the window)
|
|
||||||
timeout(sourceConnectWindowTimeout),
|
|
||||||
//make sure we're only listening to events from the "opened" window.
|
|
||||||
filter((event: MessageEvent) => event.source == openedWindow),
|
|
||||||
//after filtering, we should only have one event to handle.
|
|
||||||
first(),
|
|
||||||
map((event) => {
|
|
||||||
console.log(`received postMessage notification from ${sourceType} window & sending acknowledgment`)
|
|
||||||
// @ts-ignore
|
|
||||||
event.source.postMessage(JSON.stringify({close:true}), event.origin);
|
|
||||||
}),
|
|
||||||
concatMap(() => {
|
|
||||||
console.log("requesting authorized claim")
|
|
||||||
return this.lighthouseApi.getSourceAuthorizeClaim(sourceType, state)
|
|
||||||
}),
|
|
||||||
catchError((err) => {
|
|
||||||
console.warn(`timed out waiting for notification from ${sourceType} (${sourceConnectWindowTimeout/1000}s), closing window`)
|
|
||||||
openedWindow.self.close()
|
|
||||||
return throwError(err)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private uuidV4(){
|
|
||||||
// @ts-ignore
|
|
||||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
|
||||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async swapOauthPKCEToken(state: string, codeVerifier: any, authorizationUrl: URL, connectData: LighthouseSource, claimData: AuthorizeClaim){
|
|
||||||
// @ts-expect-error
|
|
||||||
const client: oauth.Client = {
|
|
||||||
client_id: connectData.client_id,
|
|
||||||
token_endpoint_auth_method: 'none',
|
|
||||||
}
|
|
||||||
|
|
||||||
//check if the oauth_token_endpoint_auth_methods_supported field is set
|
|
||||||
if(connectData.oauth_token_endpoint_auth_methods_supported){
|
|
||||||
let auth_methods = connectData.oauth_token_endpoint_auth_methods_supported.split(",")
|
|
||||||
client.token_endpoint_auth_method = auth_methods[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const as = {
|
|
||||||
issuer: `${authorizationUrl.protocol}//${authorizationUrl.host}`,
|
|
||||||
authorization_endpoint: connectData.oauth_authorization_endpoint,
|
|
||||||
token_endpoint: connectData.oauth_token_endpoint,
|
|
||||||
introspection_endpoint: connectData.oauth_introspection_endpoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("STARTING--- Oauth.validateAuthResponse")
|
|
||||||
const params = Oauth.validateAuthResponse(as, client, new URLSearchParams(claimData as any), state)
|
|
||||||
if (Oauth.isOAuth2Error(params)) {
|
|
||||||
console.log('error', params)
|
|
||||||
throw new Error() // Handle OAuth 2.0 redirect error
|
|
||||||
}
|
|
||||||
console.log("ENDING--- Oauth.validateAuthResponse")
|
|
||||||
console.log("STARTING--- Oauth.authorizationCodeGrantRequest")
|
|
||||||
const response = await Oauth.authorizationCodeGrantRequest(
|
|
||||||
as,
|
|
||||||
client,
|
|
||||||
params,
|
|
||||||
connectData.redirect_uri,
|
|
||||||
codeVerifier,
|
|
||||||
)
|
|
||||||
let payload = await response.json()
|
|
||||||
console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload)
|
|
||||||
return payload
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import {LighthouseSource} from '../models/lighthouse/lighthouse-source';
|
|
||||||
import {environment} from '../../environments/environment';
|
|
||||||
import {map} from 'rxjs/operators';
|
import {map} from 'rxjs/operators';
|
||||||
import {ResponseWrapper} from '../models/response-wrapper';
|
import {ResponseWrapper} from '../models/response-wrapper';
|
||||||
import {Source} from '../models/fasten/source';
|
import {Source} from '../models/fasten/source';
|
||||||
|
|
|
@ -4,8 +4,8 @@ import {Observable} from 'rxjs';
|
||||||
import {environment} from '../../environments/environment';
|
import {environment} from '../../environments/environment';
|
||||||
import {map, tap} from 'rxjs/operators';
|
import {map, tap} from 'rxjs/operators';
|
||||||
import {ResponseWrapper} from '../models/response-wrapper';
|
import {ResponseWrapper} from '../models/response-wrapper';
|
||||||
import {LighthouseSource} from '../models/lighthouse/lighthouse-source';
|
import {LighthouseSourceMetadata} from '../models/lighthouse/lighthouse-source-metadata';
|
||||||
import {AuthorizeClaim} from '../models/lighthouse/authorize-claim';
|
import * as Oauth from '@panva/oauth4webapi';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -15,57 +15,140 @@ export class LighthouseService {
|
||||||
constructor(private _httpClient: HttpClient) {
|
constructor(private _httpClient: HttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
getLighthouseSource(sourceType: string): Observable<LighthouseSource> {
|
getLighthouseSource(sourceType: string): Observable<LighthouseSourceMetadata> {
|
||||||
return this._httpClient.get<any>(`${environment.lighthouse_api_endpoint_base}/connect/${sourceType}`)
|
return this._httpClient.get<any>(`${environment.lighthouse_api_endpoint_base}/connect/${sourceType}`)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((response: ResponseWrapper) => {
|
map((response: ResponseWrapper) => {
|
||||||
return response.data as LighthouseSource
|
return response.data as LighthouseSourceMetadata
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
generatePKCESourceAuthorizeUrl(codeVerifier: string, codeChallenge: string, codeChallengeMethod: string, state: string, lighthouseSource: LighthouseSource): URL {
|
|
||||||
|
async generateSourceAuthorizeUrl(sourceType: string, lighthouseSource: LighthouseSourceMetadata): Promise<URL> {
|
||||||
|
const state = this.uuidV4()
|
||||||
|
localStorage.setItem(`${sourceType}:state`, state)
|
||||||
|
|
||||||
// generate the authorization url
|
// generate the authorization url
|
||||||
const authorizationUrl = new URL(lighthouseSource.oauth_authorization_endpoint);
|
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
|
||||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
|
||||||
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
|
|
||||||
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
|
|
||||||
authorizationUrl.searchParams.set('redirect_uri', lighthouseSource.redirect_uri);
|
authorizationUrl.searchParams.set('redirect_uri', lighthouseSource.redirect_uri);
|
||||||
authorizationUrl.searchParams.set('response_type', 'code');
|
authorizationUrl.searchParams.set('response_type', lighthouseSource.response_types_supported[0]);
|
||||||
|
authorizationUrl.searchParams.set('response_mode', 'fragment');
|
||||||
authorizationUrl.searchParams.set('state', state);
|
authorizationUrl.searchParams.set('state', state);
|
||||||
if(lighthouseSource.scopes && lighthouseSource.scopes.length){
|
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
||||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes.join(' '));
|
if(lighthouseSource.scopes_supported && lighthouseSource.scopes_supported.length){
|
||||||
|
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes_supported.join(' '));
|
||||||
}
|
}
|
||||||
if (lighthouseSource.aud) {
|
if (lighthouseSource.aud) {
|
||||||
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
|
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//this is for providers that support CORS and PKCE (public client auth)
|
||||||
|
if(!lighthouseSource.confidential){
|
||||||
|
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
||||||
|
const codeVerifier = Oauth.generateRandomCodeVerifier();
|
||||||
|
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
||||||
|
const codeChallengeMethod = lighthouseSource.code_challenge_methods_supported[0]; // 'S256'
|
||||||
|
|
||||||
|
localStorage.setItem(`${lighthouseSource}:code_verifier`, codeVerifier)
|
||||||
|
localStorage.setItem(`${sourceType}:code_challenge`, codeChallenge)
|
||||||
|
localStorage.setItem(`${sourceType}:code_challenge_method`, codeChallengeMethod)
|
||||||
|
|
||||||
|
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
|
||||||
|
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
|
||||||
|
}
|
||||||
|
|
||||||
return authorizationUrl
|
return authorizationUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
generateConfidentialSourceAuthorizeUrl(state: string, lighthouseSource: LighthouseSource): URL {
|
redirectWithOriginAndDestination(destUrl: string, sourceType: string){
|
||||||
// generate the authorization url
|
const originUrlParts = new URL(window.location.href)
|
||||||
const authorizationUrl = new URL(lighthouseSource.oauth_authorization_endpoint);
|
originUrlParts.hash = "" //reset hash in-case its present.
|
||||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
originUrlParts.pathname = this.pathJoin([originUrlParts.pathname, `callback/${sourceType}`])
|
||||||
authorizationUrl.searchParams.set('redirect_uri', lighthouseSource.redirect_uri);
|
|
||||||
authorizationUrl.searchParams.set('response_type', 'code');
|
|
||||||
authorizationUrl.searchParams.set('state', state);
|
const redirectUrlParts = new URL(`${environment.lighthouse_api_endpoint_base}/redirect/${sourceType}`);
|
||||||
if(lighthouseSource.scopes && lighthouseSource.scopes.length){
|
const redirectParams = new URLSearchParams()
|
||||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes.join(' '));
|
redirectParams.set("origin_url", originUrlParts.toString())
|
||||||
|
redirectParams.set("dest_url", destUrl)
|
||||||
|
redirectUrlParts.search = redirectParams.toString()
|
||||||
|
console.log(redirectUrlParts.toString());
|
||||||
|
|
||||||
|
// Simulate a mouse click:
|
||||||
|
window.location.href = redirectUrlParts.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async swapOauthToken(sourceType: string, sourceMetadata: LighthouseSourceMetadata, expectedState: string, state: string, code: string){
|
||||||
|
// @ts-expect-error
|
||||||
|
const client: oauth.Client = {
|
||||||
|
client_id: sourceMetadata.client_id
|
||||||
}
|
}
|
||||||
if (lighthouseSource.aud) {
|
//this is for providers that support CORS & PKCE (public client auth)
|
||||||
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
|
let codeVerifier = undefined
|
||||||
|
if(!sourceMetadata.confidential){
|
||||||
|
client.token_endpoint_auth_method = 'none'
|
||||||
|
codeVerifier = localStorage.getItem(`${sourceType}:code_verifier`)
|
||||||
|
|
||||||
|
localStorage.removeItem(`${sourceType}:code_verifier`)
|
||||||
|
localStorage.removeItem(`${sourceType}:code_challenge`)
|
||||||
|
localStorage.removeItem(`${sourceType}:code_challenge_method`)
|
||||||
|
} else {
|
||||||
|
console.log("This is a confidential client, using lighthouse token endpoint.")
|
||||||
|
//if this is a confidential client, we need to "override" token endpoint, and use the Fasten Lighthouse to complete the swap
|
||||||
|
sourceMetadata.token_endpoint = sourceMetadata.redirect_uri.replace("callback", "token")
|
||||||
|
//use a placeholder client_secret (the actual secret is stored in Lighthouse)
|
||||||
|
client.client_secret = "placeholder"
|
||||||
|
client.token_endpoint_auth_method = "client_secret_basic"
|
||||||
|
codeVerifier = "placeholder"
|
||||||
}
|
}
|
||||||
return authorizationUrl
|
|
||||||
|
const as = {
|
||||||
|
issuer: sourceMetadata.issuer,
|
||||||
|
authorization_endpoint: sourceMetadata.authorization_endpoint,
|
||||||
|
token_endpoint: sourceMetadata.token_endpoint,
|
||||||
|
introspection_endpoint: sourceMetadata.introspection_endpoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("STARTING--- Oauth.validateAuthResponse")
|
||||||
|
const params = Oauth.validateAuthResponse(as, client, new URLSearchParams({"code": code, "state": state}), expectedState)
|
||||||
|
if (Oauth.isOAuth2Error(params)) {
|
||||||
|
console.log('error', params)
|
||||||
|
throw new Error() // Handle OAuth 2.0 redirect error
|
||||||
|
}
|
||||||
|
console.log("ENDING--- Oauth.validateAuthResponse")
|
||||||
|
console.log("STARTING--- Oauth.authorizationCodeGrantRequest")
|
||||||
|
const response = await Oauth.authorizationCodeGrantRequest(
|
||||||
|
as,
|
||||||
|
client,
|
||||||
|
params,
|
||||||
|
sourceMetadata.redirect_uri,
|
||||||
|
codeVerifier,
|
||||||
|
)
|
||||||
|
let payload = await response.json()
|
||||||
|
console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload)
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getSourceAuthorizeClaim(sourceType: string, state: string): Observable<AuthorizeClaim> {
|
|
||||||
return this._httpClient.get<any>(`${environment.lighthouse_api_endpoint_base}/claim/${sourceType}`, {params: {"state": state}})
|
private pathJoin(parts: string[], sep?: string): string{
|
||||||
.pipe(
|
const separator = sep || '/';
|
||||||
map((response: ResponseWrapper) => {
|
parts = parts.map((part, index)=>{
|
||||||
return response.data as AuthorizeClaim
|
if (index) {
|
||||||
})
|
part = part.replace(new RegExp('^' + separator), '');
|
||||||
);
|
}
|
||||||
|
if (index !== parts.length - 1) {
|
||||||
|
part = part.replace(new RegExp(separator + '$'), '');
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
})
|
||||||
|
return parts.join(separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private uuidV4(){
|
||||||
|
// @ts-ignore
|
||||||
|
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||||
|
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue