Authorization Code Flow with PKCE

I have created a fork of the okta-angular library in order to implement support for the Authorization Code Flow with PKCE.

The redirect to the /authorize endpoint works as expected:

  async authorizationCodeRedirect() {

    const url = this.auth.issuer + '/v1/authorize'
      + '?response_type=' + encodeURIComponent(this.auth.responseType)
      + '&client_id=' + encodeURIComponent(this.auth.clientId)
      + '&state=' + encodeURIComponent(this.auth.state)
      + '&scope=' + encodeURIComponent(this.auth.scope)
      + '&redirect_uri=' + encodeURIComponent(this.auth.redirectUri)
      + '&code_challenge=' + encodeURIComponent(this.auth.code_challenge)
      + '&code_challenge_method=' + encodeURIComponent(this.auth.code_challenge_method);

    this.document.location.href = url;
  }

As does the POST to the /token endpoint:

  async handleAuthorizationCodeFlow() {

    const params = new URLSearchParams(this.document.location.search.substring(1));
    const code = params.get('code');
    const state = params.get('state');

    const endpoint = this.auth.issuer + '/v1/token';

    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      })
    };

    const body = {
      grant_type: 'authorization_code',
      client_id: this.auth.clientId,
      redirect_uri: this.auth.redirectUri,
      code: code,
      code_verifier: 'M25iVXpKU3puUjFaYWg3T1NDTDQtcW1ROUY5YXlwalNoc0hhakxifmZHag'
    };

    const urlEncoded = Object.keys(body).map(key => key + '=' + body[key]).join('&');

    const response = await this.http.post<any>(endpoint, urlEncoded, httpOptions).toPromise();

    ...

  }

The response is as expected:

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "eyJraWQiOiJCMHcxTjV ...",
    "scope": "openid email groups profile address phone",
    "id_token": "eyJraWQiOiJCMHcx ..."
}

However, if I try to add the tokens to the token manager:

    ...

    if (response.id_token) {
      this.oktaAuth.tokenManager.add('idToken', response.id_token);
    }

    if (response.access_token) {
      this.oktaAuth.tokenManager.add('accessToken', response.access_token);
    }

I receive the following error:

    // ERROR Error: Uncaught (in promise): AuthSdkError: Token must be an Object with scopes, expiresAt, and an idToken or
    // accessToken properties

We’re working on building direct support for the auth code + PKCE flow into the okta-angular library.

In the meantime, the okta-auth-js library (which the angular library depends on) already has this support built in AND it does the heavy lifting for you.

Check out this repo: https://github.com/dogeared/okta-auth-js-pkce-example. In particular: https://github.com/dogeared/okta-auth-js-pkce-example/blob/master/src/auth/index.js

The important bits are the setup:

const oktaAuth = new OktaAuth({
    issuer: ISSUER,
    clientId: CLIENT_ID,
    redirectUri: REDIRECT_URL,
    grantType:  'authorization_code'
});

Notice the grantType is authorization_code

the login request:

export async function loginOkta() {
    oktaAuth.token.getWithRedirect({
        responseType: 'code',
        scopes: ['openid', 'profile', 'email'],
    });
}

Notice the responseType is code

and the redirect handler:

export async function redirect() {
    oktaAuth.token.parseFromUrl()
    .then((tokens) => {
        tokens.forEach((token) => {
            if (token.idToken) {
                oktaAuth.tokenManager.add('id_token', token);
            } else if (token.accessToken) {
                oktaAuth.tokenManager.add('access_token', token);
            }
        });
        router.push('/profile');
    })
    .catch(console.error);
}

the parseFromUrl function does two things:

  1. detects that there’s a code in the url (as opposed to the implicit flow, where there would be tokens in the url)
  2. creates the POST request to the /token endpoint and executes it

parseFromUrl returns a promise which is resolved in the then() function.

It iterates over the passed in tokens and makes the appropriate call to the tokenManager.add function:

oktaAuth.tokenManager.add('id_token', token);

Notice that what’s stored in the tokenManager is NOT the raw jwt, but rather an object that includes the jwt.

That’s why you were getting the error you posted above.

HTH!

I tried to follow the approach/style adopted by the okta-angular library.

My fork of the okta-angular library is working now:

  async exchangeCodeForToken(): Promise<void> {

    const params = new URLSearchParams(this.document.location.search.substring(1));
    const code = params.get('code');
    const state = params.get('state');

    const endpoint = this.auth.issuer + '/v1/token';

    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      })
    };

    const body = {
      grant_type: 'authorization_code',
      client_id: this.auth.clientId,
      redirect_uri: this.auth.redirectUri,
      code: code,
      code_verifier: 'M25iVXpKU3puUjFaYWg3T1NDTDQtcW1ROUY5YXlwalNoc0hhakxifmZHag'
    };

    const urlEncoded = Object.keys(body).map(key => key + '=' + body[key]).join('&');

    return this.http.post<any>(endpoint, urlEncoded, httpOptions).toPromise();
  }

  async handleAuthorizationCodeFlow(): Promise<void> {

    const res = await this.exchangeCodeForToken();

    if (res['access_token']) {

      this.oktaAuth.tokenManager.add('accessToken', {
        accessToken: res['access_token'],
        expiresAt: Number(res['expires_in']) + Math.floor(Date.now() / 1000),
        tokenType: res['token_type'],
        scopes: res['scope']
      });

    }

    if (res['id_token']) {

      const jwt = this.oktaAuth.token.decode(res['id_token']);

      this.oktaAuth.tokenManager.add('idToken', {
        clientId: this.auth.clientId,
        idToken: res['id_token'],
        expiresAt: jwt.payload.exp,
        scopes: res['scope'],
        claims: jwt.payload
      });

    }

    if (await this.isAuthenticated()) {
      this.emitAuthenticationState(true);
    }

    const fromUri = this.getFromUri();
    this.router.navigate([fromUri.uri], fromUri.extras);
  }

I still need to add support for:

  • create a random ‘state’ value
  • create a PKCE code_verifier (the plaintext random secret)
  • create the code_challenge
  • verify the ‘state’ value

You’re doing a lot of heavy lifting that’s already built into the okta-auth-js library, which the okta-angular library depends on.

If you really want to deal with the code_verifier yourself, take a look at the spec. It just had to be between 43 and 128 characters.

It was never my intention to do any heavy lifting :slight_smile:

My first exposure to Okta was via the Angular Quick Start guide (see my feedback).

Then I set up some groups (see my feedback).

Then I got the Implicit Flow working using the okta-angular library (and everything that goes along with that e.g., an AuthService, an AuthGuard, a http-interceptor, etc.).

I read Aaron Parecki’s post and then compared his sample code to the code in the okta-angular library (which wraps the okta-auth-js library) that handles the Implicit Flow.

Aaron’s sample includes the source code for base64urlencode, generateRandomString, pkceChallengeFromVerifier and sha256.

I managed to create a working implementation for the Authorization Code flow (albeit with hard coded values for ‘code_challenge’ and ‘code_verifier’).

I’ve just had a quick look at the token.js source and the pkce utils library. And, I can see that you have added support for ‘state’, ‘codeChallenge’ and ‘codeVerifier’.

So based on the okta-auth-js-pkce-example I added a call to getWithRedirect():

  loginWithRedirect() {

    this.oktaAuth.token.getWithRedirect({
      responseType: 'code',
      scopes: ['openid', 'profile', 'email', 'phone', 'address', 'groups'],
    });

  }

But without any success as yet …

Sorted :slight_smile:

See: Okta (Angular) Authentication (AuthN) library

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.