0.9.1 • Published 8 months ago

@hashicorp/oidc-client-js v0.9.1

Weekly downloads
-
License
MPL-2.0
Repository
github
Last release
8 months ago

oidc-client.js

Minimal client library that provides OIDC & OAuth2 support with PKCE in Vanilla Javascript for browser-based applications.

Table of Contents
Terminology
OIDC Flow
Example Usage
Local Development
Testing
Releases
API

Terminology

  • Client: The application requesting access to resources (eg: hcp, hcp learn, etc.).
  • Resource Owner: The user who owns the resources and either grants or denies permission to access them.
  • User-agent: Here, the browser, which "retrieves, renders and facilitates end-user interaction with Web content". W3C definition
  • Authorization Server: An OIDC compliant identity provider (in our case Cloud-IdP).

Flow

  1. Generate PKCE code verifier and challenge.

    • code verifier - cryptographically random string using the characters A-Z, a-z, 0-9, and the punctuation characters - . _ ~ (hyphen, period, underscore, and tilde), between 43 and 128 characters long.
    • code challenge - BASE64-URL-encoded string of the SHA-256 hash of the code verifier.
  2. Generate state parameter, build the authorization URL (refered to in the code as the authCodeURL), and redirect user to it. This URL will have query parameters to pass to the authorization server, and the redirect.

    • state - opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent (browser) back to the client. The parameter SHOULD be used for preventing cross-site request forgery as described in Section 10.12 of RFC 6749. This is an OAuth2 protection.

    The client sends a GET request using a loginRedirect method. This method calls a redirect and passes it our authCodeURL. It redirects the resource owner to the auth page (login, logout, etc.) where the authorization server (cloud-idp) takes over.

    The state and codeVerifier need to be stored to be accessible by the application when the user is redirected back, so before the resource owner is redirected, these two values are saved to local storage.

    Example authCodeURL - used for the GET request:

http://127.0.0.1:4444/oauth2/auth?
	response_type=code
	&client_id=Gd2iwKRW0Wo7wxnxxMlWUj0q
	&redirect_uri=http://127.0.0.1:8080/
	&scope=openid
	&state=JX9uzARkSyp77LeP
	&code_challenge=nY_VpGr-e9hN3on7fBC_jGRy_DmB7f-tz6vp3PkICgw
	&code_challenge_method=S256

At this point, our authorization server (cloud-idp) takes over and, along with Auth0, will prompt the user to agree to the requested scope. The user will either agree or not to the requested scope(s) with the authorization server.

  1. Redirect, validate response from the authorization server and compare state

If the user agrees, the authorization server (http://127.0.0.1:4444 in the example above) will redirect the user to the redirect_uri, which should be the same as the one provided when the client instance was created.

The redirect URI will include some URL query parameters that were set by the authorization server. This includes the authorization code (referred to as code) and state. Authorization response

The response from the authorization server needs to be validated and the state needs to be checked against the state in local storage. If the response fails validation or the states do not match, the application should no longer continue to process the request.

  1. Get tokens

Exchange the authorization code (returned as part of the redirect URI in step 3) to get access and ID tokens from the authorization server. This is where we'll finally send the raw codeVerifier to the authorization server. To do this, the client builds a POST request to the token endpoint:

http://127.0.0.1:4444/oauth2/token

grant_type=authorization_code
&client_id=Gd2iwKRW0Wo7wxnxxMlWUj0q
&redirect_uri=http://127.0.0.1:8080/
&code=8_WL_VPqBqrRTO5qRWzDD6Gt_YXwhXkRIt6dF1yQZomsSXYm
&code_verifier=TtumKq_N6QFdLtXRIkDSzenumkm83JZ9VGmUZn-X5TAnX_T_

NOTE: Hydra expects the body of the POST to be application/x-www-form-urlencoded. If the authorization server call is hosted at a different origin, Hydra will need to be setup to support CORS for the public endpoint; and the Origin header sent by the client to that endpoint must match the configured allowed origins by Hydra to match the returned Access-Control-Allow-Origin.

Example request, with curl:

$ curl 'http://127.0.0.1:4444/oauth2/token' \
	-H 'Content-Type: application/x-www-form-urlencoded' \
	-H 'Origin: http://127.0.0.1:8080' \
	--data-raw 'grant_type=authorization_code&client_id=my-client&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2F&scope=openid&code=EXBkFVeSXOxFvG35yl4BwUCRGf_ONkkSam_haZaxry8.5U5mDAG1UGxl5LeR2EqZMj5bq94R7FisPHLE5WlN-8k&code_verifier=tdfZ4EX8cob-8MmfbvISx-fdV69wW9hlAaLJDxq_EOqHZnKpD8WGSxMZFxgLXYTBJtEXrsuv1xEfh2gXty87dQ'

Example response headers:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://127.0.0.1:8080
Content-Length: 1379
Content-Type: application/json;charset=UTF-8
...

Example Usage

The client includes the loginRedirect, getTokens, and getTokensSilently functions for a simple API.

// Ensure an OIDC client is initialized.
var client = new OIDCClient(
	'my-client',
	'http://127.0.0.1:8080',
	'http://127.0.0.1:4444'
);

// Ensure that it is ready, has the provider configuration.
await client.waitUntilReady();

// Or ensure the client is initialzed and ready together:
// var client = await new OIDCClient("my-client", "", "http://127.0.0.1:8080/", "http://127.0.0.1:4444").waitUntilReady();

// Redirect the user to the login page.
client.loginRedirect();

// Now the user has been redirected back (and the OIDC client is initialized/ready), get the tokens.
var tokensResp = await client.getTokens();
// tokensResp.id_token
// tokensResp.access_token

// Or, depending on the user's cookies, silently re-authenticate the user via an iframe.
var tokensResp2 = await client.getTokensSilently(
	{
		prompt: 'none',
	} /* Required for local Hydra demo, do not use for Cloud IDP */
);

It also exposes other functions that can be used, for stricter control:

var client = new OIDCClient(
	'my-client',
	'http://127.0.0.1:8080/',
	'http://127.0.0.1:4444'
);
// OIDCClient {clientID: 'my-client', redirectURI: 'http://127.0.0.1:8080/', providerConfiguration: {…}, tokenURL: 'http://127.0.0.1:4444/oauth2/token', …}

// Ensure that it is ready, has the provider configuration.
await client.waitUntilReady();

// Or ensure the client is initialzed and ready together:
// var client = await new OIDCClient("my-client", "http://127.0.0.1:8080/", "http://127.0.0.1:4444").waitUntilReady();

var codeVerifier = client.generateCodeVerifier();
// '0DYn79LoDZZBMfWPF7oIMKgjWkOWv46RBVY3/bzpILBp6WMXAlxdEBPQUY9bUcZtXWs2C0lOaklyM4yf3qUbAQ=='

var codeChallenge = await client.generateCodeChallenge(codeVerifier);
// 'vdRlwFjGEbwmnfGX1VZi7WWKL8-dqg-SYRbiNKsextM'

var state = client.generateState();
// 'NARGjkyjN4cPYW9pFZ6ZuorpyjsusxGMKPFzAW03VQbC5MC2557b7+BzjgPzC8osWmojmgpbOaTby0OTEa64Pg=='

// store critical information before redirecting to auth code URL
localStorage['code'] = codeVerifier;
localStorage['state'] = state;

var authCodeURL = client.authCodeURL(state, codeChallenge);
// 'https://.../?response_type=code&client_id=...&state=...&code=...

// Add Auth0 screen_hint
// var authCodeURL = client.authCodeURL(state, codeChallenge, {screen_hint: "signup"})

// If the user has a session, already logged in before.
// var authCodeURL = client.authCodeURL(state, codeChallenge, {login_hint: "foo@bar.com", prompt: "none"})

// To use a scope other than just "openid".
// var authCodeURL = client.authCodeURL(state, codeChallenge, {scope: "openid offline"})

// redirect user to consent
client.redirect(authCodeURL.toString());

// Now the user has been redirected back to this page after consenting with authorization server (will need to create new client again)
var redirectedURL = new URL(window.location.href);
// redirectedURL.searchParams.get("code")
// redirectedURL.searchParams.get("scope")
// redirectedURL.searchParams.get("state")

// Create a new client for verifying the data returned by the Authorization Server
var client = new OIDCClient(
	'my-client',
	'http://127.0.0.1:8080/',
	'http://127.0.0.1:4444'
);
// check state
client.verifyStateFromAuthorizationServer(
	localStorage['state'],
	redirectedURL.searchParams.get('state')
);
// true

// check no error occured during login after checking state
client.validateAuthorizationServerResponse(redirectedURL);
// true

var resp = await client.exchangeAuthorizationCodeForTokens(
	redirectedURL.searchParams.get('code'),
	localStorage['code']
);
// {access_token: '25zX72vbw5VVAnkBipgl_0Z6nqdJc0qma1rV6bydjfU.rrle-OdErBZdmhkI8Zm6heP4H-C-mdwg2ssIcronygs', expires_in: 3600, id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpiNWRkNTljOS…8VBH22EdAWnNvjUwBDy410H4MNLhGJrOK1Ak0hgbuP470GN6U', scope: 'openid', token_type: 'bearer'}

resp.id_token;
// 'eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpiNWRkNTljOS03NWU4LTQwYWQtOTMyMi03NTg2NmUxNWRjYzEifQ.eyJhY3IiOiIwIiwiYXRfaGFzaCI6InZ5LVk1OWFXeGlmTXBGcHhFa0dxbWciLCJhdWQiOlsibXktY2xpZW50Il0sImF1dGhfdGltZSI6MTY0MjAwMjczMCwiZXhwIjoxNjQyMDA2MzU5LCJpYXQiOjE2NDIwMDI3NTksImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NDQ0NC8iLCJqdGkiOiIxMzUyN2YzMC1kYjA2LTRlZWMtYjQyZC02YzFkNTYzNzEwMTYiLCJub25jZSI6IlcyOWlhbVZqZENCQmNuSmhlVUoxWm1abGNsMD0iLCJyYXQiOjE2NDIwMDI3MjQsInNpZCI6IjNjYjBiY2FkLWRiM2MtNDVjNi1iZjc1LTY0YjEzYjNjMTQ2OCIsInN1YiI6ImZvb0BiYXIuY29tIn0.ccltzIjCjlbF2o8KTQPMSEXFTdXeIsh6bz-SgMOyXMVIq6LwqvtOiu4FBK31vVF5Hvx5sTqmqlMLyMUbMxGTAwluf_XqTC3wnRvzDkpIXQHRYLafY7ZgqXpKUk43fa1nVqdJRG0E0Ah0FNNJIKWyC-59v7g_DwOrN6VHMcuElKyDDlmPF803tA8pECLXS7xvaEFiXNhQFFE2CECfl7W8Rv9HvDhJduwZmNcmdt57vc7Sepw3MtpF-HcvqGPk-Nel8pkAs51Gn3Zb4SKH2jWchfFgvZ3rOei44FWvkLVGZYXA13bf-E6t0mB2mS_d9woZzOdk8EPCGV1xTSwfs05L5U8K_mQAMSz8EuzCdtX9H7cxy4JfJ-AqIzatE1vZkcaCIdeg24s-dAp_3nki4_b3LwsoSKyyZ-_xw07HGhsWq_CR2ELIi-2pmo7L64HGMz1QUF4rD-rcSSnSsOArwA9X_zYplRAO-ZDNzx9DsK9AkUYa-C7OhDWT5CZMVVs4OAFsu0uV1qHDo3eyye9s7QK1XKx8yAhdudnUqx7tjyt9_GtVZXzIqXEroy1GLSmqV76NNyA-XdF0IHzNBwgrLvxibQkztpQrOkfKK2hrvujVB8Rt2DDRvLZSR1LlAu8VBH22EdAWnNvjUwBDy410H4MNLhGJrOK1Ak0hgbuP470GN6U'

resp.access_token;
// '25zX72vbw5VVAnkBipgl_0Z6nqdJc0qma1rV6bydjfU.rrle-OdErBZdmhkI8Zm6heP4H-C-mdwg2ssIcronygs'

resp.expires_in;
// 3600

var token = await client.validateIDToken(resp.id_token);
// OIDCToken {raw: 'eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpiNWRkNTljOS…9bKgAAb6rtdk6LMRg_AusffZVtT64JgHh5wm6p6Wrbo5TTNjE', header: {…}, payload: {…}, signature: 'eb32nBiTTPdgObvQPYjEWRSkmOrQW8FS3cwD6m35uf_KNVnJ3z…9bKgAAb6rtdk6LMRg_AusffZVtT64JgHh5wm6p6Wrbo5TTNjE'}

token.payload.sub;
// 'foo@bar.com'

// Attempt to silently auth the user, assuming they have cached credentials, such a cookies, that'll make this work:
var resp3 = await client.exchangeAuthorizationCodeForTokensAgainButHidden(
	{
		prompt: none,
	} /* Required for local Hydra demo, do not use for Cloud IDP */
);
// {access_token: 'ckGClWpX9BAXdoj8NBS7ODzSFIn4iKa3GFo4BqjgXcg.BQT-muJUpZ5c1GmBsMWR0o3poxaoOyS7jTc19S678c8', expires_in: 3599, id_token: 'eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpiNWRkNTljOS…yiMNYa8zKRtT9GLS9g4MkSuJCGtu0yZM4dfunSGEuE1Ue3s1E', scope: 'openid', token_type: 'bearer'}

Local Development

Prerequesites

The example/ directory has a simple single-page web application to test the client against a basic Hydra server. In order to make any changes to the client itself, modify the src/*.ts files directly. You can use the browser console, and follow the usage steps to complete an OIDC/OAuth2 flow with PKCE to obtain an id_token.

Build steps

  1. Make sure Docker is actively running on your computer.
  2. In one terminal window run npm run hydra.
  3. In another terminal window run npm run start.
  4. Open the browser tab to http://127.0.0.1:8080.

Note: the Vite JS is used as the build tool/HTTP server. It expects any client side env vars to be prefixed with VITE_.

Using silent auth with iframe

To use in the example app, be sure to select "Do not ask me again" on the consent page when initially logging in.

Linking with cloud-ui

How to have local changes of the OIDC client appear in cloud-ui:

  1. From the OIDC client directory run npm run build:watch. This will build changes you make to the client any time you save.
  2. From the cloud-ui directory run yarn link ../oidc-client.js. Make sure the path is to your local oidc-client.js.
  3. Open cloud-ui/hcp/ember-cli-build.js in a code editor and add the following to the autoImport key. For more information on why see the ember-auto-import docs.
autoImport: {
  watchDependencies: ['@hashicorp/oidc-client-js'],
}
  1. Start up cloud-ui. Now any change made locally to the OIDC client will reboot the ember server and appear in cloud-ui.

Testing

This repository uses Playwright.dev for E2E testing of the client. Smoke tests are set up in /tests/smoke.spec.mjs to confirm that the example application is ready to run. E2E tests in /tests/e2e.spec.mjs verify and validate the client's general functionality.

Test results are saved in /test-results only if test(s) fail. To change this behavior when modifying files locally, you can update the config.use.trace value in playwright.config.js; see Playwright documentation for more information.

Running tests

Please refer to the local development instructions and set up the example application first. Then:

  1. In the root directory, create an .env file with the following credentials:
E2E_BROWSER_EMAIL=foo@bar.com
E2E_BROWSER_PASSWORD='foobar'
PROVIDER_CONFIGURATION_TOKEN_URL=http://127.0.0.1:4444/oauth2/token
  1. Run npx playwright install
  2. Run npm run hydra
  3. Start your server, npm start
  4. Run the tests, npm test
> @hashicorp/oidc-client-js@0.1.1 test
> npx playwright test smoke e2e


Running 5 tests using 2 workers
tests/smoke.spec.mjs:16:3 › Smoke Tests: Demo app loads › Login page loads

To run specific unit or e2e tests, see the package.json file for CLI commands.

Releases

We publish the library to npm for use with other applications. Do the following to make a release:

  1. Create a new branch from the latest version of main that you want to release.
  2. We try to follow semver for versioning. Keeping that in mind, from the terminal run npm version patch|minor|major. The npm version command will update package.json and package-lock.json versions and will automatically commit those changes. Please see Undoing the npm command if you need to roll back the changes.
  3. Open a PR to merge the changes into main.

  4. After the PR has been approved and merged into main, switch to your local main branch and pull the latest changes with your merged work.

  5. Create a git tag. The tag_name should match the new version number. For example, if the new package version that you just merged is v0.8.1, then the tag_name should be v0.8.1. From the main branch:
  • Run git tag <tag_name>
  • Run git push origin <tag_name>
  1. Go to the main page of the repo on GitHub and click "Create a new release".
  2. Click the "Choose a tag" dropdown and select the tag that you created.
  3. Click "Generate release notes" to have all the commits after the last release get added to the release notes. Please feel free to edit the notes to read better, if commit titles are not descriptive or include unnecessary information.
  4. Click "Publish release" to create the release on GitHub. This will start a GitHub action that will publish to npm for us.

Undoing the npm command

If you run the npm version patch|minor|major, but then realize that you need to make a change, you will need to do two things:

  1. Remove the commit of the new version using git reset --hard HEAD^
  2. Run git tag -d <tag_name> to delete the tag that was created by the npm command. For example: git tag -d v0.7.3

Now you can make any other changes you need on your feature branch. Commit your changes, then you can return to the release flow and run the npm version command.

API

OIDC Client

Type: class Parameters:

interface Parameters {
	clientID: string;
	redirectURI: string;
	issuerURI: string;
	loggerOptions?: {
		captureError?: (message: any, ...optionalParams: any[]) => void;
		level?: 'debug' | 'info' | 'default' | 'warn' | 'error';
	}; // will default to `default` level
}

Purpose: Creates an instance of an OIDC client Example Usage:

var client = new OIDCClient(
	myClientId,
	myRedirectURI,
	myIssuerURI,
	loggerOptions
);

loginRedirect

Type: function Parameters: extraOptions: object e.g. login hints, app state, etc.

⭐ T I P | If using Cloud-IdP, screen_hint=signup is expected in this object to take the user directly to Auth0’s sign up screen rather than login. If screen_hint=signin is included or completely omitted, the user will instead be taken to Auth0’s login screen.

Purpose: Generates PKCE code verifier and code challenge and redirects to the authorization server’s authorization endpoint (specified via issuerURI) with the following arguments:

  • extraOptions
  • PKCE code challenge
  • Random, securely generated state
  • Pre-defined scope of openid

Example Usage: client.loginRedirect(myExtraOptions);

getTokens

Type: function Parameters: none Purpose: Verifies & validates the state of the authorization server’s response. If successful, calls the authorization server’s token endpoint with authorization code as an argument and returns validated tokens. Example Usage: client.getTokens();

getTokensSilently

Type: function Parameters: none Purpose: Creates a dynamically-appended, hidden iframe that will generate PKCE code verifier and code challenge, and redirect to the authorization server’s authorization endpoint (specified via issuerURI) with the same arguments as loginRedirect. Finally, it’ll complete the same steps as outlined in getToken and remove the hidden iframe. Example Usage: client.getTokensSilently();

getUserInfo

Type: function Parameters: accessToken: string Purpose: Performs a call to the authorization server’s user info endpoint (specified via the issuerURI) and returns the response. Example Usage: client.getUserInfo(myAccessToken);

getIdTokenClaims

Type: function Parameters: idToken: string Purpose: Returns all claims stored in an ID token. Example Usage: client.getIdTokenClaims(myIdToken);

0.9.0

8 months ago

0.9.1

8 months ago

0.8.1

1 year ago

0.8.0

1 year ago

0.7.2

1 year ago

0.7.1

1 year ago

0.7.3

1 year ago

0.5.0

1 year ago

0.7.0

1 year ago

0.5.2

1 year ago

0.6.0

1 year ago

0.5.1

1 year ago

0.4.2

2 years ago

0.4.1

2 years ago

0.3.0

2 years ago

0.4.0

2 years ago

0.2.0

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago