handle authorize claim for confidential sources.
update BlueButton image to use Medicare logo.
This commit is contained in:
parent
4afbd6f834
commit
5f2f99a202
|
@ -9,6 +9,7 @@ const (
|
|||
SourceTypeAetna SourceType = "aetna"
|
||||
SourceTypeAthena SourceType = "athena"
|
||||
SourceTypeAnthem SourceType = "anthem"
|
||||
SourceTypeCareEvolution SourceType = "careevolution"
|
||||
SourceTypeCedarSinai SourceType = "cedarssinai"
|
||||
SourceTypeCerner SourceType = "cerner"
|
||||
SourceTypeCigna SourceType = "cigna"
|
||||
|
@ -16,6 +17,7 @@ const (
|
|||
SourceTypeDeltaDental SourceType = "deltadental"
|
||||
SourceTypeDignityHealth SourceType = "dignityhealth"
|
||||
SourceTypeEpic SourceType = "epic"
|
||||
SourceTypeHealthIT SourceType = "healthit"
|
||||
SourceTypeHCAHealthcare SourceType = "hcahealthcare"
|
||||
SourceTypeHumana SourceType = "humana"
|
||||
SourceTypeKaiser SourceType = "kaiser"
|
||||
|
@ -27,6 +29,6 @@ const (
|
|||
SourceTypeTrinity SourceType = "trinity"
|
||||
SourceTypeUCSF SourceType = "ucsf"
|
||||
SourceTypeUnitedHealthcare SourceType = "unitedhealthcare"
|
||||
SourceTypeVeteransHealthAdministration SourceType = "bluebutton"
|
||||
SourceTypeBlueButtonMedicare SourceType = "bluebutton"
|
||||
SourceTypeVerity SourceType = "verity"
|
||||
)
|
||||
|
|
|
@ -10,4 +10,5 @@ type MetadataSource struct {
|
|||
Category []string `json:"category"`
|
||||
|
||||
Supported bool `json:"enabled"`
|
||||
Confidential bool `json:"confidential"`
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ type Source struct {
|
|||
ExpiresAt int64 `json:"expires_at"`
|
||||
CodeChallenge string `json:"code_challenge"`
|
||||
CodeVerifier string `json:"code_verifier"`
|
||||
|
||||
Confidential bool `json:"confidential"`
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -12,6 +12,7 @@ func GetMetadataSource(c *gin.Context) {
|
|||
|
||||
string(pkg.SourceTypeLogica): {Display: "Logica (Sandbox)", SourceType: pkg.SourceTypeLogica, Category: []string{"Sandbox"}, Supported: true},
|
||||
string(pkg.SourceTypeAthena): {Display: "Athena (Sandbox)", SourceType: pkg.SourceTypeAthena, Category: []string{"Sandbox"}, Supported: true},
|
||||
string(pkg.SourceTypeHealthIT): {Display: "HealthIT (Sandbox)", SourceType: pkg.SourceTypeHealthIT, Category: []string{"Sandbox"}, Supported: true},
|
||||
|
||||
// enabled
|
||||
string(pkg.SourceTypeAetna): {Display: "Aetna", SourceType: pkg.SourceTypeAetna, Category: []string{"Insurance"}, Supported: true},
|
||||
|
@ -20,9 +21,10 @@ func GetMetadataSource(c *gin.Context) {
|
|||
//TODO: infinite pagination for Encounters??
|
||||
string(pkg.SourceTypeCerner): {Display: "Cerner (Sandbox)", SourceType: pkg.SourceTypeCerner, Category: []string{"Sandbox"}, Supported: true},
|
||||
|
||||
//does not support PKCE
|
||||
string(pkg.SourceTypeVeteransHealthAdministration): {Display: "Veterans Health (BlueButton)", SourceType: pkg.SourceTypeVeteransHealthAdministration, Category: []string{"Hospital"}, Supported: false},
|
||||
//does not support PKCE/Public Clients
|
||||
string(pkg.SourceTypeBlueButtonMedicare): {Display: "Medicare/VA Health (BlueButton)", SourceType: pkg.SourceTypeBlueButtonMedicare, Category: []string{"Hospital"}, Supported: false},
|
||||
string(pkg.SourceTypeEpic): {Display: "Epic (Sandbox)", SourceType: pkg.SourceTypeEpic, Category: []string{"Sandbox"}, Supported: false},
|
||||
string(pkg.SourceTypeCareEvolution): {Display: "CareEvolution (Sandbox)", SourceType: pkg.SourceTypeCareEvolution, Category: []string{"Sandbox"}, Supported: false},
|
||||
|
||||
// pending
|
||||
string(pkg.SourceTypeAnthem): {Display: "Anthem", SourceType: pkg.SourceTypeAnthem, Category: []string{"Insurance"}},
|
||||
|
|
|
@ -20,4 +20,6 @@ export class Source {
|
|||
expires_at: number
|
||||
code_challenge: string
|
||||
code_verifier: string
|
||||
|
||||
confidential: boolean
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
export class AuthorizeClaim {
|
||||
source_type: string
|
||||
state: string
|
||||
code: string
|
||||
ttl: number
|
||||
code?: string
|
||||
ttl?: number
|
||||
|
||||
access_token?: string
|
||||
refresh_token?: string
|
||||
id_token?: string
|
||||
expires_at?: number
|
||||
}
|
||||
|
|
|
@ -11,4 +11,6 @@ export class LighthouseSource {
|
|||
scopes: string[]
|
||||
redirect_uri: string
|
||||
aud: string
|
||||
|
||||
confidential: boolean
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
|
||||
<div class="modal-footer">
|
||||
<button (click)="syncSource(modalSourceInfo.source)" type="button" class="btn btn-indigo">Sync</button>
|
||||
<button type="button" class="btn btn-outline-light">Reconnect</button>
|
||||
<button (click)="connect($event, modalSourceInfo.source['source_type'])" type="button" class="btn btn-outline-light">Reconnect</button>
|
||||
<button type="button" class="btn btn-outline-danger">Delete</button>
|
||||
<button type="button" class="btn btn-outline-light">Close</button>
|
||||
</div>
|
||||
|
|
|
@ -77,13 +77,25 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
.subscribe(async (connectData: LighthouseSource) => {
|
||||
console.log(connectData);
|
||||
|
||||
// https://github.com/panva/oauth4webapi/blob/8eba19eac408bdec5c1fe8abac2710c50bfadcc3/examples/public.ts
|
||||
const codeVerifier = Oauth.generateRandomCodeVerifier();
|
||||
const codeChallenge = await Oauth.calculatePKCECodeChallenge(codeVerifier);
|
||||
const codeChallengeMethod = 'S256';
|
||||
const state = this.uuidV4()
|
||||
|
||||
const authorizationUrl = this.lighthouseApi.generatePKCESourceAuthorizeUrl(codeVerifier, codeChallenge, codeChallengeMethod, state, connectData)
|
||||
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());
|
||||
// open new browser window
|
||||
|
@ -94,50 +106,20 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
console.log("claim response:", claimData)
|
||||
this.status[sourceType] = "token"
|
||||
|
||||
//swap code for token
|
||||
let sub: string
|
||||
let access_token: string
|
||||
let payload: any
|
||||
if(connectData.confidential){
|
||||
|
||||
// @ts-expect-error
|
||||
const client: oauth.Client = {
|
||||
client_id: connectData.client_id,
|
||||
token_endpoint_auth_method: 'none',
|
||||
// we should have an access_token (and optionally a refresh_token) in the claim
|
||||
payload = claimData
|
||||
|
||||
} else {
|
||||
payload = await this.swapOauthPKCEToken(state, codeVerifier, authorizationUrl, connectData, claimData)
|
||||
}
|
||||
|
||||
//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,
|
||||
)
|
||||
const payload = await response.json()
|
||||
console.log("ENDING--- Oauth.authorizationCodeGrantRequest", payload)
|
||||
|
||||
|
||||
//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){
|
||||
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)
|
||||
|
@ -145,6 +127,8 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
payload.patient = jwtDecode(payload.id_token, new BrowserAdapter())["profile"].replace(/^(Patient\/)/,'')
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Create FHIR Client
|
||||
|
||||
const sourceCredential: Source = {
|
||||
|
@ -167,7 +151,7 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
|
||||
// @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(
|
||||
|
@ -267,4 +251,45 @@ export class MedicalSourcesComponent implements OnInit {
|
|||
(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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -42,6 +42,23 @@ export class LighthouseService {
|
|||
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(' '));
|
||||
}
|
||||
if (lighthouseSource.aud) {
|
||||
authorizationUrl.searchParams.set('aud', lighthouseSource.aud);
|
||||
}
|
||||
return authorizationUrl
|
||||
}
|
||||
|
||||
|
||||
getSourceAuthorizeClaim(sourceType: string, state: string): Observable<AuthorizeClaim> {
|
||||
return this._httpClient.get<any>(`${environment.lighthouse_api_endpoint_base}/claim/${sourceType}`, {params: {"state": state}})
|
||||
.pipe(
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 4.4 KiB |
Loading…
Reference in New Issue