Implementing authorization code grant OpenID/OAuth with AppAuth-JS 1.3.1 may require these workarounds:
- The public API for
RedirectRequestHandler
does not expose an auth flow completion method that gives negative feedback; if the callback does not contain proper authorization results (token, code etc.) the currentcompleteAuthorizationRequestIfPossible()
method fails silently with a console log, not giving any indication to the caller. To overcome this, you can subclassRedirectRequestHandler
to change (expose) the already existing internalcompleteAuthorizationRequest()
method fromprotected
topublic
:import {RedirectRequestHandler} from '@openid/appauth'; // .. class CallableRedirectRequestHandler extends RedirectRequestHandler { completeAuthorizationRequest() { return super.completeAuthorizationRequest(); } }
Afterwards you can modify the auth completion path to use aCallableRedirectRequestHandler
instance, and directly callcompleteAuthorizationRequest()
, with a null-check on the result included in the returnedPromise
to handle missing/failed auth cases.
- Callback URLs from code grants usually contain result/return data (
code
,state
etc.) as query params, unlike implicit grant which typically uses a hash (fortoken
,state
etc.). However the defaultBasicQueryStringUtils
always parses the callback URL assuming a hash format. When code grant is in use, one possible workaround is to extendparse()
method ofBasicQueryStringUtils
, to always override/turn offuseHash
when it is invoked internally:import {BasicQueryStringUtils} from '@openid/appauth'; // .. class NoHashQueryStringUtils extends BasicQueryStringUtils { parse(input, useHash?) { return super.parse(input, false); } }
With these changes, an authorization code grant flow with AppAuth-JS in Angular may look like this:
import { AuthorizationRequest, AuthorizationServiceConfiguration, RedirectRequestHandler, BasicQueryStringUtils } from '@openid/appauth'; // your OpenID provider configs const config = { baseUrl: "your OpenID provider's base URL", clientId: "client ID", scopes: "space separated scopes list", tokenName: "key of the token field returned in the back-end code-exchange response; in case it is customized/changeable" }; // ... component code /* call this method to start the auth process AppAuth will store request details into localStorage, and recover it from there during the next/completion phase */ async initiateAuth() { let serverConfig; try { serverConfig = await AuthorizationServiceConfiguration.fetchFromIssuer(config.baseUrl); } catch (e) { throw new Error(`Error loading auth server details: ${e.message}`); } const handler = new RedirectRequestHandler(); const request = new AuthorizationRequest({ client_id: config.clientId, redirect_uri: `${window.location.origin}/openid-callback`, // your callback URL scope: config.scopes || 'openid', response_type: AuthorizationRequest.RESPONSE_TYPE_CODE }); handler.performAuthorizationRequest(serverConfig, request); } /* call this method when callback URL/page has loaded if previously saved details could be loaded from localStorage, and return parameters could be extracted successfully from the (callback) URL, completion call will receive an auth result; otherwise it will receive an empty/undefined value, in which case you can notify the user that auth was unsuccessful */ async completeAuth() { const authResult = await new CallableRedirectRequestHandler(undefined, new NoHashQueryStringUtils()) .completeAuthorizationRequest(); if (!authResult) { throw new Error('Could not find an authentication attempt to proceed; did you open this page directly, without going through log-in?'); } let contentHeader: Headers = new Headers(); contentHeader.append('Content-Type', 'application/json'); return this.http.post(/* your code-exchange backend URL */, JSON.stringify({ code: authResult.response.code, verifier: authResult.request.internal['code_verifier'] }), { headers: contentHeader }).toPromise() .then(response => response.json()[config.tokenName || 'id_token']) .catch(e => /* handle it */); } // ..