Skip to content

Authorizing a Website or Mobile App

To get authorization to act on the user's behalf, your website or mobile app would use what OAuth 2.0 calls the authorization code flow. It is called so because the authorization server issues your app a special single-use string, called the authorization code, which is then used once to obtain the access token — the authentication secret that will actually allow access to protected APIs.

The process involves four steps:

  1. Your app crafts an authorization URL that it opens in the user's browser.
  2. The authorization server renders the consent page, where the user is able to review the permissions your apps requests, and grant all, some, or none of them.
  3. The authorization server redirects the browser to a URL under your control, which receives the authorization code as a query parameter.
  4. Your app redeems this authorization code for the access token, which you then pass to protected APIs.

Let's review this process step by step. If you're interested in further details, you can read the chapter "Accessing Data in an OAuth Server" in the online book OAuth 2.0 Simplified.

The Central Archives OAuth Demo is an example of implementing this flow in the browser, without server-side code, and without using a library. (This is not recommended, and is done purely for illustrative purposes.)

Redirecting to the authorization URL

First, your app crafts the authorization URL, which will be opened as a web page to be seen by the user. The URL begins with this hostname and path:

https://centralarchives.org/oauth/authorize

We also need to add seven mandatory query parameters to it.

  • client_id: Your app's client ID. You can find it on the app edit page. Example: 4bdef73a-3e39-49fb-bb4b-685cc2650901.
  • scope: The list of scopes (permissions) you're requesting, as a single string with individual scopes separated by spaces. Example: idp:character:?.read idp:user.read idp:user:email.read. For more details, see Scopes and Permissions.
  • redirect_uri: An URI under your app's control, where the authorization server will redirect the browser on either success or error. It must exactly, character for character, match once of the redirect URIs you entered when registering your app.
  • state: Any nonempty string, which will be passed back to your redirect URI. It can be any data that will let your app identify this particular request.
  • response_type: The value of this query paramater must be code.
  • code_challenge_method: The value of this query paramater must be S256.
  • code_challenge: A single-use value unique to this request, your PKCE code challenge. If you're using a client library, it will typically take care of computing this for you. If you're doing everything manually (not recommended), refer to the link for the details on computing a code challenge.

Computing the Code Challenge

If you're doing everything manually, here's the quick version. First, generate a random alphanumeric string, which will be the code verifier. Store it somewhere, as you'll need it in the last step. The code challenge is computed as base64url(sha256(utf8_bytes(code_verifier))). You can find a sample TypeScript implementation here.

If your app generates this URL on a web server, it can use the HTTP Location header to redirect the user. A JavaScript client running in a browser can use window.location.assign() to navigate to the constructed URL.

Sample code

const AUTHORIZE_URL = 'https://centralarchives.org/oauth/authorize';
const CLIENT_ID = '4bdef73a-3e39-49fb-bb4b-685cc2650901';
const scopes = ['idp:character:?.read', 'idp:user.read', 'idp:user:email.read'];
const redirectUri = 'https://my-app.example/path';

// Generate random values
const state = generateRandomString();
const codeVerifier = generateRandomString();
const codeChallenge = await pkceChallengeFromVerifier(codeVerifier);
window.localStorage.setItem(`codeVerifier_${state}`, codeVerifier);

// Build URL
const oauthUrl = new URL(AUTHORIZE_URL);
oauthUrl.searchParams.append('client_id', clientId);
oauthUrl.searchParams.append('redirect_uri', redirectUri);
oauthUrl.searchParams.append('scope', scopes.join(' '));
oauthUrl.searchParams.append('state', state);
oauthUrl.searchParams.append('response_type', 'code');
oauthUrl.searchParams.append('code_challenge_method', 'S256');
oauthUrl.searchParams.append('code_challenge', codeChallenge);

// Redirect
window.location.assign(oauthUrl);
const client = new OAuth2Client({
  clientId: '4bdef73a-3e39-49fb-bb4b-685cc2650901',
  server: 'https://centralarchives.org',
  authorizationEndpoint: '/oauth/authorize',
});

const scopes = ['idp:character:?.read', 'idp:user.read', 'idp:user:email.read'];

// Generate random values
const state = generateRandomString();
const codeVerifier = generateRandomString();
window.localStorage.setItem(`codeVerifier_${state}`, codeVerifier);

// Build URL

const oauthUrl = await client.authorizationCode.getAuthorizeUri({
  redirectUri: 'https://my-app.example/path',
  state,
  scope: scopes,
  codeVerifier,
});

// Redirect
window.location.assign(oauthUrl);
using System.Diagnostics;
using System.Net.Http;
using System.Security.Cryptography;
using IdentityModel;
using IdentityModel.Client;

public class OAuthClient {
  private static readonly string AuthorizeUrl = "https://centralarchives.org/oauth/authorize";
  private static readonly string ClientId = "4bdef73a-3e39-49fb-bb4b-685cc2650901";
  private static readonly string Scopes = "idp:character:?.read idp:user.read idp:user:email.read";

  private string state;
  private string codeVerifier;

  public void InitiateAuthorization()
  {
    // Generate random values
    state = CryptoRandom.CreateUniqueId(32);
    codeVerifier = CryptoRandom.CreateUniqueId(32);

    var codeChallenge = Base64Url.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier)));
    // Assuming the mobile app has registered custom URLs of the scheme com.myapp
    var redirectUri = "com.myapp://redirect";

    // Build URL
    var oauthUrl = new RequestUrl(AuthorizeUrl).CreateAuthorizeUrl(
        clientId: ClientId,
        scope: Scopes,
        redirectUri: redirectUri,
        state: state,
        codeChallenge: codeChallenge,
        codeChallengeMethod: "S256",
        responseType: "code"
    );

    // Redirect
    Process.Start(new ProcessStartInfo { FileName = oauthUrl, UseShellExecute = true });
  }
}
import jakarta.ws.rs.POST;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
// Also import your favorite random string generation library

private static final String AUTHORIZE_URL = "https://centralarchives.org/oauth/authorize";
private static final String CLIENT_ID = "4bdef73a-3e39-49fb-bb4b-685cc2650901";
private static final String SCOPES = "idp:character:?.read idp:user.read idp:user:email.read";

@POST
public Response initiateAuthorization() throws Exception {
  var state = generateRandomString();
  var codeVerifier = generateRandomString();
  database.storeRequestData(state, codeVerifier);

  var codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(
    MessageDigest.getInstance("SHA-256").digest(
      codeVerifier.getBytes(StandardCharsets.UTF_8)
    )
  );

  var redirectUri = "https://my-app.example/central-archives-callback";
  var oauthUrl = UriBuilder.fromUri(AUTHORIZE_URL)
      .queryParam("client_id", CLIENT_ID)
      .queryParam("redirect_uri", redirectUri)
      .queryParam("scope", SCOPES)
      .queryParam("state", state)
      .queryParam("response_type", "code")
      .queryParam("code_challenge_method", "S256")
      .queryParam("code_challenge", codeChallenge)
      .build();

  // Redirect with 303 See Other to force GET method
  return Response.seeOther(oauthUrl);
}

Once the authorization page opens, assuming the request is well-formed and the user hasn't blocklisted your app outright, the browser will show the user the consent page.

Code flow consent

The user must approve at least one of the scopes your app has requested (besides offline_access), or deny the request outright. Therefore, if the authorization server redirects with a successful response, your app can be certain that at least one of the requested scopes (besides offline_access) was granted.

When the user clicks the Authorize button, the authorization server will redirect to the redirect URI you provided, with two query parameters: code and state.

  • code: the authorization code, which you will redeem for the access token in the next step.
  • state: the state string you provided when creating the authorization request.

If the user denies the request, or an error occurs, the redirect URI will receive two different query parameters instead:

  • error: the machine-readable error code, as a string constant.
  • error_description: the human-readable error description.

Exchanging the authorization code for the access token

After obtaining the code and state parameters from your redirect URI, you will now exchange the authorization code for an access token.

To do this, you make an HTTP POST request in background to the token API endpoint

https://centralarchives.org/oauth/token

The request body is an HTML form, of the application/x-www-form-urlencoded content type. It has the following form parameters:

  • code: The authorization code, passed to you via the code parameter in your redirect URI.
  • redirect_uri: The redirect URI you used to obtain the code. Must be exactly identical to the redirect_uri you passed to the original authorization request.
  • code_verifier: The code verifier string you generated for the authorization request.
  • grant_type: The value of this form parameter must be authorization_code.

You also need to pass your client ID and (if your app has one) client secret. The way of doing so depends on the type of your app:

  • Server-side apps have a client secret. They must authenticate the POST request with HTTP Basic authentication, with the client ID as the username, and the client secret as the password.
  • Web and native apps don't have a client secret. They must instead pass their client_id as an additional, fifth form parameter.

The server will reply with a JSON response of the following type:

{
  "token_type": "Bearer",
  "access_token": "<access token>",
  "refresh_token": "<refresh token (if offline_access was requested, otherwise absent)>",
  "expires_in": <seconds until the access token expires>,
  "scope": "<scopes actually granted, separated by space>"
}

The access_token value is what you will pass in the Authorization: Bearer header when calling protected APIs.

Sample code

const TOKEN_URL = 'https://centralarchives.org/oauth/token';
const CLIENT_ID = '4bdef73a-3e39-49fb-bb4b-685cc2650901';

const locationUrl = new URL(window.location.href);

if (locationUrl.searchParams.get('error')) {
  throw new Error(locationUrl.searchParams.get('error_description'));
}

const codeVerifier = window.localStorage.getItem(
  `codeVerifier_${locationUrl.searchParams.get('state')}`
);

// Retrieve access token
const formData = new URLSearchParams();
formData.append('client_id', CLIENT_ID);
formData.append('grant_type', 'authorization_code');
formData.append('redirect_uri', redirectUri);
formData.append('code', locationUrl.searchParams.get('code'));
formData.append('code_verifier', codeVerifier);

const response = await fetch(TOKEN_URL, {
  method: 'POST',
  body: formData
});

return (await response.json()).access_token;
const client = new OAuth2Client({
  clientId: '4bdef73a-3e39-49fb-bb4b-685cc2650901',
  server: 'https://centralarchives.org',
  tokenEndpoint: '/oauth/token',
});

const locationUrl = new URL(window.location.href);

if (locationUrl.searchParams.get('error')) {
  throw new Error(locationUrl.searchParams.get('error_description'));
}

const state = locationUrl.searchParams.get('state');
const codeVerifier = window.localStorage.getItem(
  `codeVerifier_${state}`
);

// Retrieve access token
const tokenResponse = await client.authorizationCode.getTokenFromCodeRedirect(
  window.location.href,
  {
    redirectUri: 'https://my-app.example/path',
    state,
    codeVerifier,
  }
);

return tokenResponse.accessToken;
using System.Diagnostics;
using System.Net.Http;
using System.Security.Cryptography;
using System.Web;
using IdentityModel;
using IdentityModel.Client;

public class OAuthClient {
  private static readonly string TokenUrl = "https://centralarchives.org/oauth/token";
  private static readonly string ClientId = "4bdef73a-3e39-49fb-bb4b-685cc2650901";

  private string state;
  private string codeVerifier;

  public void InitiateAuthorization() { <... see above> }

  public async Task<string> GetAccessToken(string fullRedirectUri)
  {
    // Assuming the mobile app has registered custom URLs of the scheme com.myapp
    var redirectUri = "com.myapp://redirect";
    var receivedQueryParams = HttpUtility.ParseQueryString(
        new UriBuilder(fullRedirectUri).Query);

    if (!string.IsNullOrEmpty(receivedQueryParams["error"]))
    {
      throw new HttpRequestException(receivedQueryParams["error_description"]);
    }

    var code = receivedQueryParams["code"];

    using var httpClient = new HttpClient();
    var response = await httpClient.RequestAuthorizationCodeTokenAsync(
      new AuthorizationCodeTokenRequest {
        Address = TokenUrl,
        ClientId = ClientId,
        GrantType = "authorization_code",
        Code = code,
        RedirectUri = redirectUri,
        CodeVerifier = codeVerifier
      }
    );

    if (response.IsError)
    {
      throw new HttpRequestException(response.ErrorDescription);
    }

    return response.AccessToken;
  }
}
import jakarta.ws.rs.GET;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.Form;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import com.fasterxml.jackson.annotation.JsonProperty;

private static final String TOKEN_URL = "https://centralarchives.org/oauth/token";
private static final String CLIENT_ID = "4bdef73a-3e39-49fb-bb4b-685cc2650901";

class TokenResponse {
  @JsonProperty("token_type") private String tokenType;
  @JsonProperty("access_token") private String accessToken;
  @JsonProperty("refresh_token") private String refreshToken;
  @JsonProperty("expires_in") private int expiresIn;
  @JsonProperty("scope") private String scope;

  // Getters and setters omitted for brevity
}

@GET('central-archives-callback')
public Response onCentralArchivesRedirect(
    @QueryParam("code") String code,
    @QueryParam("state") String state,
    @QueryParam("error") String errorCode,
    @QueryParam("error_description") String errorDescription) throws Exception {
  if (errorCode != null && !errorCode.isEmpty()) {
    return renderErrorPage(errorCode, errorDescription);
  }

  var codeVerifier = database.loadRequestData(state);
  var redirectUri = "https://my-app.example/central-archives-callback";

  var form = new Form();
  form.param("client_id", CLIENT_ID);
  form.param("grant_type", "authorization_code");
  form.param("redirect_uri", redirectUri);
  form.param("code", code);
  form.param("code_verifier", codeVerifier);

  String accessToken;

  try (var client = ClientBuilder.newClient()) {
    var requestBody = Entity.entity(form,
        MediaType.APPLICATION_FORM_URLENCODED_TYPE);

    TokenResponse response = client.target(TOKEN_URL)
        .request(MediaType.APPLICATION_JSON_TYPE)
        .post(requestBody, TokenResponse.class);
    accessToken = response.getAccessToken();
  } catch (Exception e) {
    return renderErrorPage(e);
  }

  // Do something with the access token (e.g. call an API)...
}

Note

For illustrative purposes, the code samples on this page do only basic error checking.