Exploring OpenID Connect configuration: Key fields and their uses
Tags: webdev, identity, developer, opensource
Published: 2024-05-23 00:42:35

Explores the key fields and practical applications of OpenID Connect configuration.

---

In today’s digital world, authentication has become a central component of securing websites and applications. OpenID Connect (OIDC), an authentication layer built on top of the OAuth 2.0 protocol, offers a standardized and straightforward way to authenticate identities.

However, properly integrating OIDC can be daunting, especially for newcomers. A good starting point is often the OIDC configuration file, usually found at the `{authServerUrl}/.well-known/openid-configuration` URL, which contains all necessary details for implementing OIDC services.

This guide aims to clarify the key fields within this configuration, helping developers grasp their importance and practical applications to better integrate OIDC into their projects.

# Analyzing OIDC configuration fields
The OIDC configuration is a JSON file similar to the following:

```
{
  "authorization_endpoint": "<http://localhost:3001/oidc/auth>",
  "claims_parameter_supported": false,
  "claims_supported": [
    "sub",
    "name",
    "profile",
    "email",
    "email_verified",
    "phone_number",
    "phone_number_verified"
  ],
  "code_challenge_methods_supported": ["S256"],
  "grant_types_supported": [
    "implicit",
    "authorization_code",
    "refresh_token",
    "client_credentials"
  ],
  "issuer": "<http://localhost:3001/oidc>",
  "jwks_uri": "<http://localhost:3001/oidc/jwks>",
  "response_modes_supported": ["form_post", "fragment", "query"],
  "response_types_supported": ["code id_token", "code", "id_token", "none"],
  "scopes_supported": ["openid", "offline_access", "profile", "email", "phone"],
  "subject_types_supported": ["public"],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_jwt",
    "client_secret_post"
  ],
  "token_endpoint_auth_signing_alg_values_supported": ["HS256"],
  "token_endpoint": "<http://localhost:3001/oidc/token>",
  "userinfo_endpoint": "<http://localhost:3001/oidc/me>"
}
```
Next, we will delve deeper into some of the key fields.


### authorization_endpoint
When integrating OIDC services, the first step usually involves handling user login requests within the application. This process includes redirecting users to the authorization server's `authorization_endpoint`. This endpoint is the server address where authorization requests are sent, guiding users to the login page for authentication.

When making a request to the `authorization_endpoint`, it must be configured with additional query parameters for each authorization:

- `response_type`: Specifies the type of authorization returned. This typically includes "code" (for the authorization code flow), "token" (for the implicit flow), and "id_token token" or "code id_token" (for the hybrid flow), which can be found in the response_types_supported field of the OIDC configuration.
- `client_id`: A unique identifier assigned to the application by the authorization server when the app is registered. This ID is used by the authorization server to recognize the client application initiating the request.
- `scope`: Defines the requested access scope, specifying the resources or user information accessible. Common scopes include "openid" (mandatory), "profile", and "email", among others. You can find the supported scopes in the OIDC configuration's scopes_supported field; different scopes should be separated by spaces.
- `redirect_uri`: After login or authorization, the authorization server redirects the user back to the URI provided by the application. This URI must strictly match the URI provided during the application's registration.
- `state`: A randomly generated string used to protect the client from cross-site request forgery attacks. Consistency of the state between the authorization request and callback must be maintained to ensure security.

These parameters allow developers to precisely control the behavior of OIDC authorization requests, ensuring they are secure and meet the needs of the application.

```
// Build the authorization request URL
function createAuthRequestURL(clientId, redirectUri, scope, responseType, state) {
  const url = new URL(config.authorization_endpoint);
  url.searchParams.append('response_type', responseType);
  url.searchParams.append('client_id', clientId);

  // Redirect URI to which the user is redirected after login
  url.searchParams.append('redirect_uri', redirectUri);

  // Requested permission scope, such as "openid email profile"
  url.searchParams.append('scope', scope);

  // A random state value to prevent CSRF attacks
  url.searchParams.append('state', state);

  return url.toString();
}
```
After completing the request to the `authorization_endpoint` and passing through authentication, users are redirected to the specified `redirect_uri`, which will include a very crucial query parameter—`code`. This authorization code is provided by the authorization server and is the result of the user authenticating and authorizing the app to access their information at the authorization server.

### token_endpoint
`token_endpoint` is the server endpoint used by the authorization server to exchange the aforementioned authorization code for access and refresh tokens. In the OAuth 2.0 authorization code flow, this step is critical as it involves converting the obtained authorization code into actual access tokens, which the app can use to access the user's protected resources.
Here is how the exchange of the authorization code for access tokens is implemented (note that this example uses the `client_secret_post` authentication method. For other supported authentication methods, refer to the later analysis of the
`token_endpoint_auth_methods_supported` field):

```
// Exchange the authorization code for access tokens
async function fetchTokens(code, redirectUri, clientId, clientSecret) {
  const response = await fetch(config.token_endpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: `code=${code}&redirect_uri=${redirectUri}&client_id=${clientId}&client_secret=${clientSecret}&grant_type=authorization_code`,
  });
  return await response.json();
}
```
In this code, we first send a request to the `token_endpoint` using the `POST` method. The content type is set to `application/x-www-form-urlencoded`, which is required by most authorization servers. The request body includes the following parameters:

- `code`: The authorization code obtained from the authorization server, which is returned via the `redirectUri` after the user completes the authorization.
- `redirect_uri`: The same redirect URI used to obtain the authorization code, used to verify the legitimacy of the request.
- `client_id`: The client identifier of the application, used to identify the application making the request.
- `client_secret`: The client secret of the application, used to verify the application's identity.
- `grant_type`: Specifies the type of authorization, using `"authorization_code"` here, which indicates that the access token is obtained through the authorization code.

This function is executed asynchronously and returns a JSON object containing the access token, which the application can use to request user data or perform other operations.

### userinfo_endpoint
`userinfo_endpoint` is the server endpoint used by the authorization server to obtain detailed information about authenticated users. After users successfully authorize and obtain access tokens through the `token_endpoint`, the application can request this endpoint to access the user's profile information, such as username and email address. The specific information obtained depends on the `scope` specified by the user in the authentication request.

```
// Retrieve user information
async function fetchUserInfo(accessToken, tokenType) {
  const response = await fetch(config.userinfo_endpoint, {
    method: 'GET', // Call userinfo_endpoint using the GET method
    headers: {
      /**
       * Use the token type and access token.
       * The token type returned along with the access token by the token endpoint
       */
      Authorization: `${tokenType} ${accessToken}`,
    },
  });
  if (!response.ok) {
    throw new Error('Failed to fetch user info.');
  }
  return await response.json(); // Parse and return user information in JSON format
}
```
In this function, we first use the `GET` method to request the `userinfo_endpoint`, including the `Authorization` field in the request header composed of the `token_type` and `accessToken`. This is a standard operation according to the OAuth 2.0 specification, ensuring the secure retrieval of user information. Additionally, the scope of user information accessed through the access token completely depends on the `scope` parameter values adopted by the user during the authorization initiation. For example, if the `email` scope is used, the response should include the user's email address.

### issuer & jwks_uri
The issuer identifies the URL of the authorization server, while the `jwks_uri` provides the URI for the JSON Web Key Set (JWKS), which is a set of public keys used to verify JWT signatures. Verifying the JWT's `issuer` and signature are basic steps in ensuring token security, but in real scenarios, the verification process typically also includes checking the token's validity period, audience, authorization scope, and other important fields.

The following code primarily demonstrates how to use the `issuer` and `jwks_uri` together to verify a JWT:

```
import { jwtVerify } from 'jose';

// Assume you have obtained the OIDC configuration
const config = {
  issuer: '<https://example.com>',
  jwks_uri: '<https://example.com/jwks>',
};

// Fetch JWKS
async function fetchJWKS() {
  const response = await fetch(config.jwks_uri);
  const { keys } = await response.json();
  return keys;
}

// Validate token
async function validateToken(token, audience) {
  try {
    const jwks = await fetchJWKS();
    const { payload } = await jwtVerify(token, jwks, {
      issuer: config.issuer,
      audience,
    });
    console.log('Token validated successfully:', payload);
    return true;
  } catch (error) {
    console.error('Failed to validate token:', error);
    return false;
  }
}
```
### scopes_supported & claims_supported
The `claims_supported` and `scopes_supported` fields declare the user attributes (claims) and access scopes (scopes) supported by the authorization server. They help clients understand which user attributes and access scopes are supported by the authorization server, enabling clients to effectively construct authorization requests and parse responses.

When constructing an authorization request, clients can specify the required `scope` based on the needs of the application. `Scope` defines the resources and operations that the client requests access to, such as `openid`, `email`, `profile`, and others.

It's important to note that the specific claims accessible in each authorization request vary based on the scope values specified at the start of the authentication request. For instance, the email scope grants access to the user's email address, while the phone scope provides access to their phone number. Therefore, clients should carefully choose the relevant scope to match the application's needs when crafting an authorization request, ensuring they can retrieve the necessary user attributes:

```
// Build the authorization request URL
function createAuthRequestURL(clientId, redirectUri, scope, responseType) {
  const url = new URL(config.authorization_endpoint);
  url.searchParams.append('response_type', responseType);
  url.searchParams.append('client_id', clientId);
  url.searchParams.append('redirect_uri', redirectUri);
  // Specify the required scope in the authorization request
  url.searchParams.append('scope', scope);

  return url.toString();
}
```
### token_endpoint_auth_methods_supported
The `token_endpoint_auth_methods_supported` field indicates the authentication methods supported by the token endpoint. These methods are used by the client to authenticate with the authorization server when exchanging the authorization code for access tokens.
When authenticating using the token endpoint, common authentication methods include `client_secret_post`, `client_secret_basic` and `client_secret_jwt`.

- `client_secret_post`: The client uses its client identifier and client secret to construct a form-encoded body, which is sent as part of the request body. We have already seen an example of this method in the `token_endpoint` section above.
- `client_secret_basic`: The client uses its client identifier and client secret to construct a basic authentication header, which is added to the request header.

```
// Use the client_secret_basic authentication method
const headersBasic = new Headers();
headersBasic.append(
  'Authorization',
  `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`
);

// Then send the request with the basic authentication header to obtain the access token
// ...
```

- `client_secret_jwt`: The client uses a JWT (JSON Web Token) as a client assertion to authenticate. This JWT contains the client identifier and some additional metadata and is signed using the client secret. After receiving the request, the authorization server verifies the client's identity by validating the JWT's signature.

```
// Use the client_secret_jwt authentication method
const clientSecretJwt = await new SignJWT({
  iss: clientId,
  sub: clientId,
  aud: tokenEndpoint,
  jti: randomString(),
  exp: Math.floor(Date.now() / 1000) + 600, // Expiration time is 10 minutes
  iat: Math.floor(Date.now() / 1000),
})
  .setProtectedHeader({
    alg: 'HS256',
  })
  .sign(Buffer.from(clientSecret));
// Add the JWT assertion to the request body
const params = new URLSearchParams();
params.append('grant_type', 'authorization_code');
params.append('code', authorizationCode);
// Add the client assertion parameters
params.append('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
params.append('client_assertion', clientSecretJwt);

// Send the request and obtain the access token
const tokenResponse = await fetch(tokenEndpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: params,
});
```
In practical applications, clients need to select the appropriate authentication method based on the methods supported by the authorization server, and ensure that the authentication information is correctly added to the request to securely exchange the authorization code for access tokens.

# Summary
We've now explored the key fields in the OIDC configuration, focusing on their purposes and applications. Understanding these details will enhance your grasp of OpenID Connect, empowering you to integrate and utilize it more effectively in your projects. Armed with this knowledge, you're better equipped to harness the full potential of OIDC for more secure and efficient user authentication solutions.

Logto as an authentication service built on OIDC, which offers a comprehensive suite of SDKs in various languages along with numerous integration tutorials, enables you to seamlessly integrate OAuth / OIDC logins into your applications in just minutes. If you're looking for a reliable and easy-to-use OIDC service, give Logto a try today!


{% cta https://logto.io/?ref=dev %} Try Logto Cloud for free {% endcta %}