@hashicorp/oidc-client-js
v0.9.2
Published
Minimal client library that provides OIDC & OAuth2 support with PKCE in Vanilla Javascript for browser-based applications.
Downloads
998
Maintainers
Readme
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
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.
- code verifier - cryptographically random string using the characters
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 aredirect
and passes it ourauthCodeURL
. It redirects the resource owner to the auth page (login, logout, etc.) where the authorization server (cloud-idp) takes over.The
state
andcodeVerifier
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.
- 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.
- 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: "[email protected]", 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;
// '[email protected]'
// 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
- Make sure Docker is actively running on your computer.
- In one terminal window run
npm run hydra
. - In another terminal window run
npm run start
. - 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:
- From the OIDC client directory run
npm run build:watch
. This will build changes you make to the client any time you save. - From the cloud-ui directory run
yarn link ../oidc-client.js
. Make sure the path is to your local oidc-client.js. - 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'],
}
- 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:
- In the root directory, create an
.env
file with the following credentials:
[email protected]
E2E_BROWSER_PASSWORD='foobar'
PROVIDER_CONFIGURATION_TOKEN_URL=http://127.0.0.1:4444/oauth2/token
- Run
npx playwright install
- Run
npm run hydra
- Start your server,
npm start
- Run the tests,
npm test
> @hashicorp/[email protected] 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:
Create a new branch from the latest version of
main
that you want to release.We try to follow semver for versioning. Keeping that in mind, from the terminal run
npm version patch|minor|major
. Thenpm version
command will updatepackage.json
andpackage-lock.json
versions and will automatically commit those changes. Please see Undoing the npm command if you need to roll back the changes.Open a PR to merge the changes into
main
.After the PR has been approved and merged into
main
, switch to your localmain
branch and pull the latest changes with your merged work.Create a git tag. The
tag_name
should match the new version number. For example, if the new package version that you just merged isv0.8.1
, then thetag_name
should bev0.8.1
. From themain
branch:
- Run
git tag <tag_name>
- Run
git push origin <tag_name>
- Publish a new release to start a GitHub action:
- Draft a new release at https://github.com/hashicorp/oidc-client.js/releases/ and click the "Draft a new release" button.
- Select a tag in the "Choose a tag" dropdown and select the tag that you created.
- Generate release notes by clicking the "Generate release notes" button 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.
- Publish release by clicking the "Publish release" button 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:
- Remove the commit of the new version using
git reset --hard HEAD^
- 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);