The OAuth Code Flow is one of the more typical and flexible token flows, and, with that, one of the most popular. The details of this flow are not covered by this article, but can be found in the code flow overview article on the Curity Web site.
Proof Key for Code Exchange (PKCE) is a technique described in RFC7636, and is used to mitigate the risk of the authorization code being hijacked. More details on how to configure the Curity Identity Server to enable PKCE can be found in the configuring PKCE in Curity resource page, and further details on PKCE can also be found on the same site.
The rest of this writeup explains how these technologies can be used in the JavaScript programming language. It is intentionally simple, so that the concepts are not obscured by superfluous details.
The client -- the HTML page -- needs to be configured with the client ID. In this example, the ID is public-test-client
. If certain scopes are desired, these should be configured as well.
const clientId = "public-test-client";
This function generates a random string (the verifier) that is later signed before it is sent to the authorization server, to Curity.
function generateRandomString(length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
The Web crypto API is used to hash the verifier using SHA-256. This transformed version is called the code challenge.
async function generateCodeChallenge(codeVerifier) {
var digest = await crypto.subtle.digest("SHA-256",
new TextEncoder().encode(codeVerifier));
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
Note that Javascript crypto services require that the index.html
is served in a secure context — either from (*.)localhost or via HTTPS.
You can add an /etc/hosts
entry like 127.0.0.1 public-test-client.localhost
and load the site from there, enable SSL using something like letsencrypt, or refer to this stackoverflow article for more alternatives.
If Javascript crypto is not available the script will fall back to using a plain-text code challenge.
Store the verification key between requests (using session storage).
window.sessionStorage.setItem("code_verifier", codeVerifier);
The code challenge (the transformed, temporary verification secret) is passed to the authorization server as part of the authorization request. The method (S256
, in our case) used to transform the secret is also passed with the request.
var redirectUri = window.location.href.split('?')[0];
var args = new URLSearchParams({
response_type: "code",
client_id: clientId,
code_challenge_method: "S256",
code_challenge: codeChallenge,
redirect_uri: redirectUri
});
window.location = authorizeEndpoint + "/?" + args;
The authorization code is passed in the POST request to the token endpoint along with the secret verifier key (retrieved from the session storage).
xhr.responseType = 'json';
xhr.open("POST", tokenEndpoint, true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(new URLSearchParams({
client_id: clientId,
code_verifier: window.sessionStorage.getItem("code_verifier"),
grant_type: "authorization_code",
redirect_uri: location.href.replace(location.search, ''),
code: code
}));
The OAuth server needs to be configured with a client that matches the one configured above. Also, the redirect should be set. When using npx
(described below), this will be http://localhost:8080
by default if no port is provided. Additionally, scopes may be configured.
This can be created in the Curity Identity Server by merging this XML with the current configuration:
<config xmlns="http://tail-f.com/ns/config/1.0">
<profiles xmlns="https://curity.se/ns/conf/base">
<profile>
<id>my-good-oauth-profile</id> <!-- Replace with the ID of your OAuth profile -->
<type xmlns:as="https://curity.se/ns/conf/profile/oauth">as:oauth-service</type>
<settings>
<authorization-server xmlns="https://curity.se/ns/conf/profile/oauth">
<client-store>
<config-backed>
<client>
<id>public-test-client</id>
<no-authentication>true</no-authentication>
<redirect-uris>http://localhost:8080/</redirect-uris> <!-- Update with your URL -->
<capabilities>
<code/>
</capabilities>
<validate-port-on-loopback-interfaces>false</validate-port-on-loopback-interfaces>
</client>
</config-backed>
</client-store>
</authorization-server>
</settings>
</profile>
</profiles>
</config>
The HTML needs to be served somehow from a Web server. Because the client is just a static HTML page, this can be done with a trivial server configuration. These are a couple of different ways to very easily server the static HTML page:
$ npx http-server -p <port>
$ php -S <host>:<port>
$ python -m SimpleHTTPServer <port>
These will not use TLS, but are fast and easy ways to serve the HTML file without setting up any infrastructure.
The code and samples in this repository are licensed under the Apache 2 license.
For questions and comments, contact Curity AB:
Copyright (C) 2020 Curity AB.