Implement a simple client-side OIDC SDK
Logto offers a variety of SDKs for different platforms. Apart from our official SDKs, we encourage developers from the community to create their own user-friendly SDKs. This article will guide you on building a basic client-side SDK for OIDC.
Introduction
Logto provides our developers and business groups a comprehensive Customer Identity and Access Management (CIAM) solution. We provide a wide range of ready-to-use SDKs for different platforms and application frameworks. Combined with our Logto cloud service, you can effortlessly establish a highly secure user authorization flow for your application in a matter of minutes. As a company that was born from the developer community, Logto embraces and values our community engagement. In addition to those officially developed Logto SDKs, we continuously encourage and warmly welcome developers from the community to contribute their expertise by creating more diverse and user-friendly SDKs, fulfilling the unique needs of various platforms and frameworks. In this article, we will briefly demonstrate how to implement an OIDC standard auth SDK step-by-step.
Context
The OpenID Connect (OIDC) flow is an authentication protocol that builds upon the OAuth 2.0 framework to provide identity verification and single sign-on capabilities. It allows users to authenticate themselves with an application and get authorized for further access to any private resources securely. Please refer to the OIDC specs for more details.
Workflow
A standard authorization-code flow includes the following steps:
Authentication flow
- User initiates the sign-in request: An anonymous user comes to your application from a public entrance. Attempts to get authenticated and maybe further request to access a protected resource on a third party application or service.
- User authentication: The client app generates an authentication URI and sends a request to the authorization server, which redirects the user to its sign-in page. The user interacts with the sign-in page using a wide range of sign-in methods and get authenticated by the authorization server.
- Handle sign-in callback: Upon a successful authentication, the user will be redirected back to your application with a granted
authorization_code
. Thisauthorization_code
contains all the relevant permissions linked to the authentication status and requested authorization data. - Token exchange: Request for token exchange using the
authorization_code
extracted from the redirect address above. In return:id_token
: A digitally signed JWT that contains identity information about the authenticated user.access_token
: A opaqueaccess_token
that can be used to access the user basic info endpoint.refresh_token
: Credential token allowing the user to maintain continuous exchange for anaccess_token
Authorization flow
- Accessing user information: To access more user information, the application can make additional requests to the UserInfo endpoint, utilizing the opaque
access_token
obtained from the initial token exchange flow. This enables retrieval of additional details about the user, such as their email address or profile picture. - Granting access to the protected resource: If necessary, the application can make additional requests to the token exchange endpoint, utilizing the
refresh_token
combined withresource
andscope
parameters, to obtain a dedicatedaccess_token
for the user to access the target resource. This process results in the issuance of a JWT formattedaccess_token
containing all the necessary authorization information to access the protected resource.
Implementation
We will follow some design strategies within our @logto/client JavaScript SDK to demonstrate the process of implementing a simple SDK for your own client application. Please bear in mind that the detailed code structure may differ depending on the client framework you are working with. Feel free to choose any of the Logto official SDKs as an example for your own SDK project.
Preview
Constructor
The constructor should take a logtoConfig as its input. This provides all the necessary configs you will need to establish an auth connection through this SDK.
Depending on the platform or framework you are using for the SDK, you may pass in a persistent local storage instance to the constructor. This storage instance will be used to store all the authorization-related tokens and secrets.
Init user authentication
Before generating an authentication request URL, it is essential to complete several preparation steps to ensure a secure process.
Get OIDC configs from the authorization server
Define a private method `getOidcConfigs`` to fetch OIDC configurations from the authorization server's discovery endpoint. The OIDC configurations response contains all the metadata information that the client can use to interact with the authorization server, including its endpoint locations and the server's capabilities. (Please refer to OAuth OAuth Authorization Server Metadata Specs for more details.)
PKCE Generator
A PKCE(Proof Key for Code Exchange) validation flow is essential for all the public client authorization code exchange flows. It mitigates the risk of the authorization_code interception attack. Thus a code_challenge
and code_verifier
is required for all the public client applications(e.g. native app and SPA) authorization requests.
The implementation methods make vary depends on the languages and frameworks you are using. Please refer to the code_challenge and code_verifier spec for the detailed definitions.
Generate state parameter
In the authorization flow, the state parameter is a randomly generated value that is included in the authorization request sent by the client. It acts as a security measure to prevent cross-site request forgery (CSRF) attacks.
Store intermediate session info
There are several parameters that need to be preserved in the storage for validation purpose after the user get authenticated and redirected back to the client side. We are going to implement a method to set those intermediate parameters to the storage.
Sign-in
Let's wrapping up everything implemented above, define a method generating a user sign-in URL and redirect the user to the authorization server to get authenticated.
Handle user sign-in callback
In the previous section, we created a sign-in method that generates a URL for user authentication. This URL contains all the required parameters needed to initiate an authentication flow from a client app. The method redirects the user to the authorization server’s sign-in page for authentication. After a successful sign-in, the end user will be redirected back to the redirect_uri location provided above. All the necessary parameters will be carried in the redirect_uri to complete the following token exchange flows.
Extract and verify the callback URL
This validation step is extremely important to prevent any forms of forged authorization callback attacks. The callback URL MUST be carefully verified before sending a further code exchange request to the authorization server. First of all, we need to retrieve the signInSession data we stored from the app storage.
Then verify the callback URL’s parameter before sending out the token exchange request.
- Use the previously stored
redirectUri
to verify whether thecallbackUri
is the same as the one we sent out to the authorization server. - Use the previously stored
state
to verify whether the returned state is the same as the one we sent out to the authorization server. - Check if any error is returned by the authorization server
- Check the existence of the returned
authorization_code
Send the code exchange request
As the final step of the user authentication flow, we will use the returned authorization_code
to send a token exchange request and obtain the required authorization tokens. For more details on the definitions of the request parameters, please refer to the token exchange specification.
- code: the
authorization_code
we received from the callback URI - clientId: the application ID
- redirectUri: The same value is used when generating the sign-in URL for the user.
- codeVerifier: PKCE code verifier. Similar to the redirectUri, the authorization server will compare this value with the one we previously sent, ensuring the validation of the incoming token exchange request.
Handle sign-in callback
Wrapping up everything we have. Let’s build the signInCallback handle method:
As a result, the token exchange request will return the following tokens:
id_token
: OIDC idToken, a JSON Web Token (JWT) that contains identity information about the authenticated user. id_token can also be used as the Single Source of Truth(SSOT) of a user’s authentication status.access_token
: The default authorization code returned by the authorization server. Can be used to call the user info endpoint and retrieve authenticated user information.refresh_token
: (if offline_access scope present in the authorization request): This refresh token allows the client application to obtain a new access token without requiring the user to re-authenticate, granting longer-term access to the resources.expires_in
: The duration of time, in seconds, for which the access token is valid before it expires.
ID token verification
Verifying and extract claims from the id_token
is a crucial step in the authentication process to ensure the authenticity and integrity of the token. Here are the key steps involved in verifying an idToken
.
- Signature Verification: The
id_token
is digitally signed by the authorization server using its private key. The client application needs to validate the signature using the authorization server's public key. This ensures that the token has not been tampered with and was indeed issued by the legitimate authorization server. - Issuer Verification**:** Check that the "iss" (issuer) claim in the
id_token
matches the expected value, indicating that the token was issued by the correct authorization server. - Audience Verification: Ensure that the "aud" (audience) claim in the
id_token
matches the client ID of the client application, ensuring that the token is intended for the client's - Expiration Check: Verify that the "iat" (issued at) claim in the
id_token
has not passed the current time, ensuring that the token is still valid. Since there are network transaction costs we need to set a issued time tolerance when validating the received token iat claim.
The returned id_token is a standard JSON Web Token (JWT). Depending on the framework you are using, you can find various convenient JWT token validation plugins to assist in decoding and validating the token. For this example, we will utilize jose in our JavaScript SDK to facilitate token validation and decoding.
Get user info
After successful user authentication, basic user information can be retrieved from the OIDC id_token
issued by the authorization server. However, due to performance considerations, the content of the JWT token is limited. To obtain more detailed user profile information, OIDC-compliant authorization servers offer an out-of-the-box user info endpoint. This endpoint allows you to retrieve additional user profile data by requesting specific user profile scopes.
When calling a token exchange endpoint without indicating a specific API resource, the authorization server will, by default, issue an opaque type access_token
. This access_token
can only be used to access the user info endpoint, allowing retrieval of basic user profile data.
Get access token for protected resource authorization
In most cases, a client application not only requires user authentication but also needs user authorization to access certain protected resources or API endpoints. Here, we will use the refresh_token
obtained during login to acquire access_token(s)
specifically granted to manage particular resources. This allows us to obtain access to those protected APIs.
Summary
We have provided essential method implementations for the client-side app's user authentication and authorization process. Depending on your specific scenario, you can organize and optimize the SDK logic accordingly. Please note that variations may exist due to different platforms and frameworks.
For additional details, explore the SDK packages offered by Logto. We encourage more users to join the development and engage in discussions with us. Your feedback and contributions are highly valued as we continue to enhance and expand the SDK's capabilities.
Appendix
Reserved scopes
Logto reserved scopes you will need to pass in during the initial auth request. Those scopes are either OIDC-reserved or Logto-reserved fundamental scopes to complete a successful authorization flow.
Scope name | Description |
---|---|
openid | Required for granting id_token after a successful authentication. |
offline-access | Required for granting a refresh_token that allows your client application to exchange and renew an access_token off the screen. |
profile | Required for getting access to the basic user info |
Logto Config
Property name | Type | Required | Description | Default Value |
---|---|---|---|---|
appId | string | true | The unique application identifier. Generated by the authorization server to identify the client application. | |
appSecret | string | The application secret is used, along with the application ID, to verify the identity of the requester. It is required for confidential clients like Go web or Next.js web apps, and optional for public clients like native or single-page applications (SPAs) | ||
endpoint | string | true | Your authorization server root endpoint. This property will be widely used to generate authorization requests.endpoint. | |
scopes | string list | Indicates all the necessary resource scopes the user may need to be granted in order to access any given protected resources. | [reservedScopes] | |
resources | string list | All the protected resource indicators the user may request for access |