DEV Community

Palomino for Logto

Posted on • Edited on • Originally published at blog.logto.io

Exploring OpenID Connect configuration: Key fields and their uses

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>"
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
// ...
Enter fullscreen mode Exit fullscreen mode
  • 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,
});
Enter fullscreen mode Exit fullscreen mode

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!

Try Logto Cloud for free

Top comments (0)