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/resource/:resource_id', component: ResourceDetailComponent, 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 {FastenApiService} from '../../services/fasten-api.service';
|
||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
||||
import {User} from '../../models/fasten/user';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import {FastenApiService} from '../../services/fasten-api.service';
|
||||
import {LighthouseSource} from '../../models/lighthouse/lighthouse-source';
|
||||
import {Source} from '../../models/fasten/source';
|
||||
import {Router} from '@angular/router';
|
||||
import {Summary} from '../../models/fasten/summary';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {Component, HostListener, OnInit} from '@angular/core';
|
||||
import {LighthouseService} from '../../services/lighthouse.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 {AuthorizeClaim} from '../../models/lighthouse/authorize-claim';
|
||||
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 {MetadataSource} from '../../models/fasten/metadata-source';
|
||||
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)
|
||||
|
||||
|
@ -26,6 +29,9 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
private lighthouseApi: LighthouseService,
|
||||
private fastenApi: FastenApiService,
|
||||
private modalService: NgbModal,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private location: Location,
|
||||
) { }
|
||||
status: { [name: string]: string } = {}
|
||||
|
||||
|
@ -43,6 +49,11 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
this.fastenApi.getMetadataSources().subscribe((metadataSources: {[name:string]: MetadataSource}) => {
|
||||
this.metadataSources = metadataSources
|
||||
|
||||
const callbackSourceType = this.route.snapshot.paramMap.get('source_type')
|
||||
if(callbackSourceType){
|
||||
this.callback(callbackSourceType).then(console.log)
|
||||
}
|
||||
|
||||
this.fastenApi.getSources()
|
||||
.subscribe((sourceList: Source[]) => {
|
||||
|
||||
|
@ -73,112 +84,109 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
this.status[sourceType] = "authorize"
|
||||
|
||||
this.lighthouseApi.getLighthouseSource(sourceType)
|
||||
.subscribe(async (connectData: LighthouseSource) => {
|
||||
console.log(connectData);
|
||||
|
||||
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)
|
||||
}
|
||||
.subscribe(async (sourceMetadata: LighthouseSourceMetadata) => {
|
||||
console.log(sourceMetadata);
|
||||
let authorizationUrl = await this.lighthouseApi.generateSourceAuthorizeUrl(sourceType, sourceMetadata)
|
||||
|
||||
console.log('authorize url:', authorizationUrl.toString());
|
||||
// open new browser window
|
||||
let openedWindow = window.open(authorizationUrl.toString(), "_blank");
|
||||
|
||||
//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)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
})
|
||||
// redirect to lighthouse with uri's
|
||||
this.lighthouseApi.redirectWithOriginAndDestination(authorizationUrl.toString(), sourceType)
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
this.uploadedFile = [event.addedFiles[0]]
|
||||
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 {Observable} from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
import {LighthouseSource} from '../models/lighthouse/lighthouse-source';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {map} from 'rxjs/operators';
|
||||
import {ResponseWrapper} from '../models/response-wrapper';
|
||||
import {Source} from '../models/fasten/source';
|
||||
|
|
|
@ -4,8 +4,8 @@ import {Observable} from 'rxjs';
|
|||
import {environment} from '../../environments/environment';
|
||||
import {map, tap} from 'rxjs/operators';
|
||||
import {ResponseWrapper} from '../models/response-wrapper';
|
||||
import {LighthouseSource} from '../models/lighthouse/lighthouse-source';
|
||||
import {AuthorizeClaim} from '../models/lighthouse/authorize-claim';
|
||||
import {LighthouseSourceMetadata} from '../models/lighthouse/lighthouse-source-metadata';
|
||||
import * as Oauth from '@panva/oauth4webapi';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -15,57 +15,140 @@ export class LighthouseService {
|
|||
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}`)
|
||||
.pipe(
|
||||
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
|
||||
const authorizationUrl = new URL(lighthouseSource.oauth_authorization_endpoint);
|
||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
||||
authorizationUrl.searchParams.set('code_challenge', codeChallenge);
|
||||
authorizationUrl.searchParams.set('code_challenge_method', codeChallengeMethod);
|
||||
const authorizationUrl = new URL(lighthouseSource.authorization_endpoint);
|
||||
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);
|
||||
if(lighthouseSource.scopes && lighthouseSource.scopes.length){
|
||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes.join(' '));
|
||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
||||
if(lighthouseSource.scopes_supported && lighthouseSource.scopes_supported.length){
|
||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes_supported.join(' '));
|
||||
}
|
||||
if (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
|
||||
}
|
||||
|
||||
generateConfidentialSourceAuthorizeUrl(state: string, lighthouseSource: LighthouseSource): URL {
|
||||
// generate the authorization url
|
||||
const authorizationUrl = new URL(lighthouseSource.oauth_authorization_endpoint);
|
||||
authorizationUrl.searchParams.set('client_id', lighthouseSource.client_id);
|
||||
authorizationUrl.searchParams.set('redirect_uri', lighthouseSource.redirect_uri);
|
||||
authorizationUrl.searchParams.set('response_type', 'code');
|
||||
authorizationUrl.searchParams.set('state', state);
|
||||
if(lighthouseSource.scopes && lighthouseSource.scopes.length){
|
||||
authorizationUrl.searchParams.set('scope', lighthouseSource.scopes.join(' '));
|
||||
redirectWithOriginAndDestination(destUrl: string, sourceType: string){
|
||||
const originUrlParts = new URL(window.location.href)
|
||||
originUrlParts.hash = "" //reset hash in-case its present.
|
||||
originUrlParts.pathname = this.pathJoin([originUrlParts.pathname, `callback/${sourceType}`])
|
||||
|
||||
|
||||
const redirectUrlParts = new URL(`${environment.lighthouse_api_endpoint_base}/redirect/${sourceType}`);
|
||||
const redirectParams = new URLSearchParams()
|
||||
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) {
|
||||
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
|
||||
//this is for providers that support CORS & PKCE (public client auth)
|
||||
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}})
|
||||
.pipe(
|
||||
map((response: ResponseWrapper) => {
|
||||
return response.data as AuthorizeClaim
|
||||
})
|
||||
);
|
||||
|
||||
private pathJoin(parts: string[], sep?: string): string{
|
||||
const separator = sep || '/';
|
||||
parts = parts.map((part, index)=>{
|
||||
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