OAuth/OIDC Implementation Mistakes! How We Overcame Overlooked Details and Breaking Changes in the Specifications
Introduction
We have been developing Authlete, a commercial implementation of OAuth and OpenID Connect, since 2014. As of the publication of this article (February 2025), more than 10 years have passed since development began.
As evidenced by the fact that the OpenID Foundation, a global standards organization, effectively uses Authlete as a reference implementation for developing its conformance suite (a set of compatibility tests), Authlete supports numerous standard specifications and maintains a very high implementation quality.
Authlete is a global leader in supporting FAPI, a high-security profile adopted by API ecosystems in various countries. FAPI’s robust security features have made it the preferred choice for financial institutions. I’m proud that Authlete members are listed as authors of the FAPI 2.0 specifications, including FAPI 2.0 Security Profile and FAPI 2.0 Attacker Model.
Additionally, I’d like to highlight that, thanks to our contributions and the trust we have earned, major financial institutions such as Nubank (the world’s largest digital bank) and BTG Pactual (the largest investment bank in Latin America) have adopted Authlete. (cf. “Open Finance challenges: how Nubank protects customer data and optimizes operations”)
The reason Authlete is highly valued within the industry is its ability to quickly implement draft versions of the latest specifications and provide feedback on any issues to the specification authors and working groups. A recent example of this is Authlete’s implementation of the “FAPI 2.0 HTTP Signing” specification, based on RFC 9421: HTTP Message Signatures, developed by the OpenID Foundation’s FAPI working group (cf. FAPI 2.0 HTTP Signing Demo). Additionally, as a byproduct of this implementation work, Authlete released an open-source Java library for HTTP Message Signatures (authlete/http-message-signatures).
We have implemented a large number of standard specifications over the years. Among them, there have been several occasions where we had to make changes to Authlete’s implementation due to overlooked details, misinterpretations, or breaking changes in the specifications themselves. In this article, I would like to share those experiences.
Access tokens must always be associated with scopes
The scope
parameter, as described in RFC 6749: The OAuth 2.0 Authorization Framework, is explained in the sections listed below.
- Section 4.1.1 — Authorization request in the authorization code flow
- Section 4.2.1 — Authorization request in the implicit flow
- Section 4.3.1 — Token request in the resource owner password credentials flow
- Section 4.4.1 — Token request in the client credentials flow
- Section 6 — Token request in the refresh token flow
In all of these sections, scope
is marked as OPTIONAL, so we implemented Authlete under the assumption that an access token could be issued without any associated scopes.
However, one day, I was pointed to the following statement in Section 3.3. Access Token Scope, which was used to argue that an access token must always have at least one scope.
If the client omits the
scope
parameter when requesting authorization, the authorization server MUST either process the request using a pre-defined default value or fail the request indicating an invalid scope.
Until then, Authlete’s behavior was to issue an access token with no scopes if an authorization request was received without the scope
parameter and no default scopes were configured. However, modifying this behavior to strictly comply with the above requirement would have a significant impact, as previously accepted authorization requests would start being rejected. That said, we still wanted to provide a way to strictly enforce this requirement.
To address this, we introduced a boolean flag called Service.scopeRequired
. When this flag is set to true
, Authlete strictly enforces the above requirement. In the Authlete 3.0 web console (https://console.authlete.com/), the setting labeled "Requests Without Scope Parameter" corresponds to this Service.scopeRequired
flag.
Claims specified by the scope parameter can only be embedded in the ID token when an access token is not issued
Section 5.4 of OpenID Connect Core 1.0 (hereafter referred to as OIDC Core) defines special scope values as a simplified way to request specific claims.
For example, requesting the phone
scope is equivalent to requesting the phone_number
and phone_number_verified
claims. An authorization server implementation embeds these requested claims in the ID token (Section 2. ID Token) or in the response from the UserInfo endpoint (Section 5.3. UserInfo Endpoint).
However, one day, I was pointed to the following statement in Section 5.4. Requesting Claims using Scope Values, which states that claims requested via scopes should only be embedded in the ID token if an access token is not issued.
The Claims requested by the
profile
,address
, andphone
scope values are returned from the UserInfo Endpoint, as described in Section 5.3.2, when aresponse_type
value is used that results in an Access Token being issued. However, when no Access Token is issued (which is the case for theresponse_type
valueid_token
), the resulting Claims are returned in the ID Token.
The intent behind this requirement seems to be: “If an access token is issued, the client can use it to retrieve the claim values from the UserInfo endpoint, so there’s no need to embed the claims in the ID token.”
Until then, Authlete’s implementation did not consider whether an access token was issued along with the ID token when embedding the claims specified by the scope values. In other words, it unconditionally embedded the claims in the ID token.
I believe that there are very few authorization server implementations in the world that strictly follow this requirement. However, we wanted to implement behavior that adheres strictly to the specification as well.
To address this, we introduced a boolean flag called Service.claimShortcutRestrictive
, which ensures strict adherence to the above requirement when set to true
. In the Authlete web console, the option "Restrict Shortcut" corresponds to this flag.
The wording in Section 5.4 is not strongly prohibitive, so one might interpret it as “it’s fine to include claims in the ID token even when an access token is issued.” If such an interpretation were allowed, there would be no need to change Authlete’s implementation. To clarify the intent behind the specification, I reached out to industry experts who were involved in drafting the specification. Contrary to my expectations, the intent was indeed to “prohibit” this practice. As a result, we decided to introduce the Service.claimShortcutRestrictive
flag.
Refresh token rotation came to be recognized as a bad practice
The final paragraph of Section 6, “Refreshing an Access Token,” in RFC 6749 states the following:
The authorization server MAY issue a new refresh token, in which case the client MUST discard the old refresh token and replace it with the new refresh token. The authorization server MAY revoke the old refresh token after issuing a new refresh token to the client. If a new refresh token is issued, the refresh token scope MUST be identical to that of the refresh token included by the client in the request.
The rule is that as a result of the refresh token flow, the authorization server may issue a new refresh token, and in this case, the old refresh token must be invalidated. This behavior is called “refresh token rotation.”
However, in recent years, refresh token rotation has come to be regarded as a bad practice due to the frequent issues it causes. Section 5.3.2.1 of the FAPI 2.0 Security Profile states “shall not use refresh token rotation except in extraordinary circumstances.”
When a refresh token is rotated, and for reasons such as network communication failure, the client application cannot receive the results of the refresh token flow, the client ends up in a situation where “the new access token could not be received, but the used refresh token has already been invalidated,” effectively blocking further progress.
Additionally, due to some reason on the client application side (such as a user impatiently pressing the button multiple times), multiple requests for the refresh token flow may be sent in a short period of time. In this case, if the client application proceeds with the first token response it successfully received, it may end up in a situation where both the new access token and the refresh token are invalid.
To control the behavior of refresh token rotation, Authlete provides a boolean flag called Service.refreshTokenKept
. When this flag is set to true
, refresh token rotation will not occur. The flag is named refreshTokenKept
rather than something more intuitive like refreshTokenRotationEnabled
so that the flag value false
, the default value used when the flag is missing in the configuration, can ensure the previous behavior (enabling refresh token rotation).
In Authlete’s web console, the option “Enable Token Rotation” corresponds to this flag.
On the other hand, there is an alternative approach to addressing the issues with refresh token rotation. In the case where multiple token requests are received in a short period of time, the implementation of the token endpoint can follow the approach of “not issuing a new refresh token, but returning the refresh token that was most recently created.”
In general, when multiple identical change requests are made and the consistency of the result is maintained, this is called “idempotency.”
Authlete supports idempotency for refresh token rotation. By setting the Service.refreshTokenIdempotent
boolean flag to true
, idempotency is enabled. Token requests using the same refresh token received within a 60-second window will be processed idempotently. In the Authlete web console, the option labeled "Enable Idempotency" corresponds to this flag.
The scope client metadata in dynamic client registration was overlooked
In Section 2, “Client Metadata” of OpenID Connect Dynamic Client Registration 1.0 (hereafter referred to as OIDC DynReg), the metadata that can be specified when dynamically registering a client is enumerated.
There is an OAuth version of OIDC DynReg, which is RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol. In Section 2, “Client Metadata” of RFC 7591, client metadata is also enumerated, but metadata that only exists in OpenID Connect, such as id_token_signed_response_alg
, is not included in RFC 7591. Therefore, in principle, OIDC DynReg is a superset of RFC 7591.
There was a time when I thought this way.
One day, I received feedback saying, “The scope
metadata is not recognized during dynamic client registration.”. My initial reaction was, “Huh? There is no scope
in client metadata, is there?” However, after looking into it further, I found that while scope
metadata is not defined in OIDC DynReg, RFC 7591 does define scope
metadata as follows.
scope
String containing a space-separated list of scope values (as described in Section 3.3 of OAuth 2.0 [RFC6749]) that the client can use when requesting access tokens. The semantics of values in this list are service specific. If omitted, an authorization server MAY register a client with a default set of scopes.
The description of the scope
metadata suggests the expected behavior, which is “restricting the scopes that a client can request to only those specified in the scope
metadata.” For example, if the value of scope
metadata is diary:view photo:view
, then the client can only request diary:view
and photo:view
scopes. Even if the authorization server supports the openid
scope, the client would not be able to request the openid
scope, and as a result, would not be able to receive an ID token.
The tricky part is that the RFC 7591 description states, “If omitted, an authorization server MAY register a client with a default set of scopes.” If the authorization server's implementation provides the ability to set default scope sets for each client, and if the default value for the default scope set is empty, this could lead to a problematic situation. Specifically, a client created via a dynamic client registration request without scope
metadata would end up being unable to request any scopes.
Authlete has long provided the feature to restrict the set of scopes that a client can request. Therefore, it was possible to map the scope
metadata value in a dynamic client registration request to that functionality. However, if the behavior described in RFC 7591 is implemented directly, the behavior would shift dramatically. Instead of the previously expected behavior— “a client created via dynamic client registration can request any scope supported by the authorization server”—it would now be the opposite: “a client created via dynamic client registration (if the scope
metadata is not included) cannot request any scope.” This would result in a significant change in behavior.
To support both customers who do not use the scope
metadata in dynamic client registration and customers who want to use it as defined in RFC 7591, we introduced a boolean flag called Service.dcrScopeUsedAsRequestable
. When this flag is set to true
, Authlete will follow the behavior described in RFC 7591. In the Authlete web console, this flag is represented as the "DCR’s Scope Parameter" option.
Client ID should not be looked up from the authorization code
The authorization code is issued linked to the client, so it is possible to identify the client if the authorization code is available. Because of this, when the token request during the authorization code flow did not contain information to identify the client, the initial Authlete implementation looked up the client from the authorization code.
However, one day I received feedback that “looking up the client from the authorization code is not secure.” Upon reviewing RFC 6749 Section 4.1.3, I noticed that the client_id
request parameter is described as follows:
client_id
REQUIRED, if the client is not authenticating with the authorization server as described in Section 3.2.1.
When I started considering fixing the implementation, I realized that there might be customers who use public clients and send token requests in the authorization code flow without the client_id
request parameter.
To maintain backward compatibility, I decided to introduce a boolean flag called Service.missingClientIdAllowed
. If this flag is explicitly set to true
, the client is identified from the authorization code when the client_id
request parameter is missing, even though this would technically violate the specification. In the Authlete web console, this flag is represented as the “Client ID Omission” option.
I got scolded when I made the iss response parameter included by default for security reasons
RFC 9207: OAuth 2.0 Authorization Server Issuer Identification is a specification developed as a countermeasure against the mix-up attack. An authorization server that follows this specification includes the iss
response parameter in the authorization response, as shown in the example below.
HTTP/1.1 302 Found
Location: https://client.example/cb?
code=x1848ZT64p4IirMPT0R-X3141MFPTuBX-VFL_cvaplMH58
&state=ZWVlNDBlYzA1NjdkMDNhYjg3ZjUxZjAyNGQzMTM2NzI
&iss=https%3A%2F%2Fhonest.as.example
When Authlete adds a new flag, it typically defines the flag in a way that does not change the existing behavior. That is, the flag is defined such that, when the flag’s value is false
, the behavior remains the same as before.
However, if we think that changing the existing behavior is better for security reasons, the flag is defined such that, when its value is false
, the new behavior is applied. If developers want to revert to the previous behavior, they must explicitly set the flag's value to true
.
When defining a flag to control whether the iss
response parameter is included in the authorization response, we decided, for security reasons, that the new behavior—i.e., including the iss
response parameter—should occur when the flag is false
. Therefore, we named the flag Service.issSuppressed
. If developers do not want the iss
response parameter to be included, they need to explicitly set Service.issSuppressed=true
. In the Authlete web console, the flag is represented by the option “Suppress iss Response Parameter.”
I thought customers wouldn’t be troubled by the addition of the iss
parameter in the authorization response, but I was wrong. One customer accused us of silently adding the iss
response parameter during a regular update of Authlete. They questioned us about this change and demanded that it not happen again.
After being questioned so thoroughly, I made a firm decision that, from now on, “I will never change existing behavior, even for security reasons.”
The aud
claim type in the ID token was inconsistent
One day, I was asked, “Is there a reason why the aud
claim type in the ID token issued in the authorization code flow differs from that in the ID token issued in the CIBA flow?”
Upon investigation, I found that the aud
claim type in the ID token issued in the authorization code flow was an array, while in the CIBA flow, it was a single string.
Reflecting on it, I realized that there was no deep reason for the difference in types. The aud
claim in the ID token issued in the authorization code flow was made an array based on the OIDC Core 1.0 statement: “In the general case, the aud
value is an array of case-sensitive strings. In the common special case when there is one audience, the aud
value MAY be a single case-sensitive string.” At the time of implementation, I judged that making aud
a single string was an exceptional case. On the other hand, during the CIBA implementation, I simply followed the example in the specification and used a single string.
Becayse the JWT specification states that the aud
claim can be either an array or a single string, programs that receive and process the ID token must be able to handle both cases. However, the customer who inquired seemed to want the aud
claim to be unified to either an array or a single string.
To address this, I decided to implement a two-step mechanism to specify the type of the aud
claim in the ID token.
First, I created a property called Service.idTokenAudType
. The value of this property can be array
, string
, or null
, which respectively corresponds to an array type, a single string type, or the existing behavior. In the Authlete web console, the “Choose Audience Claim Format” setting corresponds to this property.
Next, I added a request parameter called idTokenAudType
to the Authlete APIs that may issue ID tokens (e.g., the /auth/authorization/issue
API). This parameter allows overriding the behavior of Service.idTokenAudType
.
The aud
claim in JWTs often sparks significant debate. In late November 2024, after lengthy discussions, the FAPI WG decided to restrict the aud
type in client assertions to a string (prohibiting arrays).
There was a discussion along the lines of, “If we want to limit the audience to one, why not enforce ‘a string or an array with a single element’?” However, the decision was made after considering this argument.
cf. FAPI PR 522: Boom! AS to enforce aud
as a single value issuer
The specification for Loopback Interface Redirection in RFC 8252 conflicts with the existing specifications
Section 7.3 of RFC 8252: OAuth 2.0 for Native Apps specifies that when the host part of the OAuth 2.0 redirection URI is a loopback IP address, the authorization server must treat the port number as variable.
This requirement conflicts with the simple string comparison process mandated by RFC 6749: The OAuth 2.0 Authorization Framework Section 3.1.2.3. Dynamic Configuration and OpenID Connect Core 1.0 Section 3.1.2.1. Authentication Request (for more details, refer to “Loopback Interface Redirection”). Therefore, if the requirements of RFC 8252 Section 7.3 are implemented as is, it would result in a violation of the specification in environments that do not follow RFC 8252.
To address this, we introduced a boolean flag called Service.loopbackRedirectionUriVariable
to toggle whether to comply with RFC 8252 Section 7.3. In the Authlete web console, the flag is represented by the “Loopback Redirection URI” setting.
The disruptive specification change caused by RFC 9101 (JAR) sparked a huge debate
The request object defined in Section 6 of OIDC Core, “Passing Request Parameters as JWTs,” was later separated into an independent specification, RFC 9101: The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR).
However, during the separation process, it was decided to make disruptive changes to the specification (i.e., without maintaining backward compatibility), which led to a massive debate within the industry. The reason it took 11 years from the start of the discussion to the publication of RFC 9101 was due to this issue.
While migrating to RFC 9101 is desirable, implementing the specification changes unconditionally could break existing systems. Therefore, we introduced a boolean flag called Service.traditionalRequestObjectProcessingApplied
, which allows switching between processing request objects using the OIDC Core method (for backward compatibility) or the RFC 9101 method. In the Authlete web console, this flag is represented by the setting “Enable JAR Compatibility.”
The PAR endpoint defined in RFC 9126: OAuth 2.0 Pushed Authorization Requests also accepts a request object, but the PAR endpoint always processes it using the RFC 9101 method. This is because RFC 9126 specifies that the processing of a request object must follow RFC 9101.
For details on the specification changes introduced by JAR, please refer to the “Implementer’s note about JAR (JWT-Secured Authorization Request).”
Since Authlete was the only one that implemented FAPI according to the specification, Authlete had to change its implementation
During the peak of the development of the UK Open Banking Profile (OBP), which claimed FAPI compliance, the FAPI specification stated the following:
shall require user authentication at LoA 3 or greater by requesting the
acr
claim as an essential claim as defined in section 5.5.1.1. of OIDC;
Based on this, Authlete implemented a strict check: it verified that the claims
request parameter (JSON) contained an acr
property, and within that, an essential
property set to true
. If this condition was not met, Authlete deemed the request invalid and returned an error response.
Naturally, the conformance suite for OBP implementations should have included a test case to verify that a server implementation returns an error when receiving a request where the essential
property is not set to true
.
However, since Authlete was the only one enforcing this strict interpretation of the specification, introducing such a test case would have resulted in all other OBP implementations failing the test.
The conformance suite development team was using Authlete to validate their test cases, meaning they couldn’t release test cases unless Authlete passed them. However, if they wrote the test case to strictly follow the FAPI specification — ensuring that essential=true
was required—then no other OBP implementation would be able to pass. While technically correct, this approach was impractical given the UK Open Banking timeline. Requiring all other OBP implementations to implement essential
property handling at that stage was simply unrealistic.
As a result, the conformance suite development team approached Authlete, asking if there was any way to work around this issue.
This led to the creation of the Service.supportedServiceProfiles
array and the OPEN_BANKING
identifier that can be included in it.
If OPEN_BANKING
is included in this array, Authlete will skip the essential
property check even when compliance with FAPI Part 2 is required.
However, a few years later, the FAPI specification itself removed the requirement to request the acr
claim as an essential claim. As a result, Authlete also removed the code that checked the value of the essential
property for the acr
claim. This means that the presence or absence of OPEN_BANKING
no longer affects the behavior, rendering this feature meaningless today.
At the time, it was unclear to what extent the UK Open Banking Profile would diverge from the FAPI specification, so a large-scale mechanism (Service.supportedServiceProfiles
) was introduced. However, if it had been known in advance that the only difference was whether to check the essential
property of the acr
claim, a simple boolean flag like Service.acrEssentialUnchecked
would have sufficed.
The case of 401 Unauthorized from the Client Configuration Endpoint was overlooked
The client configuration endpoint, defined in RFC 7592: OAuth 2.0 Dynamic Client Registration Management Protocol, allows dynamically registered clients to retrieve, update, or delete their configuration after registration.
RFC 7592 specifies that when a client does not exist, the endpoint should return a 401 Unauthorized
error. However, I overlooked this requirement.
To implement the client configuration endpoint, Authlete provides the following APIs:
/client/registration/get
/client/registration/update
/client/registration/delete
These APIs return JSON responses that include an action
property, just like other Authlete APIs. The implementation of the client configuration endpoint determines its behavior based on the value of this action
property.
For example:
- If
action
isOK
orUPDATED
, the endpoint should return200 OK
. - If
action
isBAD_REQUEST
, it should return400 Bad Request
.
Because of this mechanism, a typical implementation of the client configuration endpoint contains a switch
statement to handle different action
values, as shown in the example below.
private Response process(ClientRegistrationResponse response)
{
// 'action' in the response denotes the next action which
// this service implementation should take.
Action action = response.getAction();
// The content of the response to the client application.
String content = response.getResponseContent();
// Dispatch according to the action.
switch (action)
{
case BAD_REQUEST:
// 400 Bad Request
return ResponseUtil.badRequest(content);
case CREATED:
// 201 Created
return ResponseUtil.created(content);
case INTERNAL_SERVER_ERROR:
// 500 Internal Server Error
return ResponseUtil.internalServerError(content);
case OK:
// 200 OK
return ResponseUtil.ok(content);
case DELETED:
// 204 no content
return ResponseUtil.noContent();
case UPDATED:
// 200 OK
return ResponseUtil.ok(content);
default:
// This never happens.
throw getApiCaller().unknownAction("/api/client/registration/*", action);
}
}
However, we overlooked the fact that there are cases where the client configuration endpoint should return 401 Unauthorized
. As a result, we did not define UNAUTHORIZED
as a possible value for the action
property.
Because of this, existing sample code did not include a case UNAUTHORIZED
statement, as shown below:
case UNAUTHORIZED:
// 401 Unauthorized
return ResponseUtil.unauthorized(content, null);
Authlete’s server-side fix ensures that UNAUTHORIZED
is returned when appropriate. However, applying this fix unconditionally could lead to unintended errors in existing client configuration endpoint implementations. If client configuration endpoints are not prepared to handle an unknown action
value, they might mistakenly trigger 500 Internal Server Error
.
To prevent this issue, Authlete will only return UNAUTHORIZED
if the implementation is explicitly ready for it.
To achieve this, we introduced the Service.unauthorizedOnClientConfigSupported
boolean flag. When this flag is set to true
, Authlete will return UNAUTHORIZED
where applicable. When false
, for backward compatibility, Authlete will return BAD_REQUEST
instead (even though this does not strictly follow RFC 7592).
In the Authlete web console, this flag corresponds to the “Return UNAUTHORIZED” setting.
There was a discrepancy in the interpretation of the specification regarding the handling of the client_secret
parameter during client updates, even among native English-speaking experts
RFC 7592 Section 2.2, Client Update Request, states the following.
This request MUST include all client metadata fields as returned to the client from a previous registration, read, or update operation. The updated client metadata fields request MUST NOT include the “registration_access_token”, “registration_client_uri”,
“client_secret_expires_at”, or “client_id_issued_at” fields described in Section 3.……
The client MUST include its “client_id” field in the request, and it MUST be the same as its currently issued client identifier. If the client includes the “client_secret” field in the request, the value of this field MUST match the currently issued client secret for that client. The client MUST NOT be allowed to overwrite its existing client secret with its own chosen value.
When I read this statement, I interpreted it as follows: “All client metadata included in the response from the dynamic client registration endpoint must also be included in the client update request, without making an exception for client_secret
.”
Since Authlete’s dynamic client registration response always includes client_secret
, Authlete's implementation checked whether the client update request also included client_secret
.
However, one day, a customer inquired: “Our client application does not use client_secret
, so we do not want to store or manage it. However, we are required to provide client_secret
when updating the client. Why is this necessary?”
So, I decided to ask the author of RFC 7592 directly. The answer I received was: “Just because client_secret
is included in the registration response does not mean it was intended to be a mandatory parameter in the update request. However, if client_secret
is included in the update request, its value should be verified against the current client secret.”
Another expert was also part of this conversation, and he had interpreted the specification the same way I had. That is, his interpretation was that “if client_secret
is included in the dynamic client registration response, then it must also be included in the client update request.”
At that moment, I witnessed firsthand how differences in interpretation could arise even among native English-speaking experts when reading the same specification. I have always believed that the original specification should be referenced when implementing the specification, but this experience reinforced my stance: “if misinterpretations can occur even when reading the original English text directly, relying on a translation in another language for implementation is completely out of the question.”
In the end, after listening to the author of the specification recount the background of its development, I decided to respect his original intent and modify Authlete’s implementation accordingly. Specifically, the new behavior was changed to: “The client update request does not need to include client_secret
, but if it is included, its value must match the current client secret.”
There are times when discussing specifications in natural language leads nowhere. In the past I observed a debate about the definitions of uniqueness, multiple use, and one-time use going in circles. To break the deadlock, I presented a piece of pseudocode to illustrate the concepts. Once I did that, the debate came to a halt.
When the final version of FAPI 1.0 introduced a restriction on the lifespan of request objects, all requests started failing
When FAPI 1.0 Part 2 transitioned from Implementer’s Draft 2 to the Final version, a new requirement regarding the lifespan of request objects was introduced.
This requirement stated that the difference between the value of the exp
(Expiration Time) claim (as defined in RFC 7519, Section 4.1.4) and the nbf
(Not Before) claim (as defined in RFC 7519, Section 4.1.5) must not exceed 60 seconds.
To support this requirement, authorization server implementations needed to extract the nbf
claim from the request object. However, until then, almost no client applications explicitly included the nbf
claim in their request objects—not even the test cases in the conformance suite.
If the authorization server had immediately started enforcing the request object lifespan check, nearly all deployed client applications and conformance suite test cases would have started failing. Therefore, the request object lifespan check should only have been enforced after client applications and the conformance suite had been properly updated. A mechanism was needed to facilitate a smooth transition.
To address this, the Service.nbfOptional
boolean flag was introduced. When this flag is set to true
, the authorization server does not enforce the request object lifespan check, even in scenarios where FAPI 1.0 Part 2 compliance would normally require it. In other words, the nbf
claim is treated as optional rather than mandatory. In Authlete’s web console, this flag corresponds to the "nbf Claim" setting.
Want to reject the request if the dynamic client registration request contains an already registered software_id
Although it is not present in OIDC DynReg, RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol defines a client metadata called software_id
as follows.
software_id
A unique identifier string (e.g., a Universally Unique Identifier (UUID)) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. Unlike “client_id”, which is issued by the authorization server and SHOULD vary between instances, the “software_id” SHOULD remain the same for all instances of the client software. The “software_id” SHOULD remain the same across multiple updates or versions of the same piece of software. The value of this field is not intended to be human readable and is usually opaque to the client and authorization server.
This definition does not imply that “the server should reject dynamic client registration requests containing a registered software_id
.” Rather, there are sentences that anticipate duplication.
However, local rules in Brazil’s API ecosystem mandate that “dynamic client registration requests containing a registered software_id
should be rejected.”
Since Authlete already had multiple customers in Brazil before this requirement was added, we had to address this request. However, since this is a local rule, it should not become the default behavior of Authlete. Therefore, we created a boolean flag, Service.dcrDuplicateSoftwareIdBlocked
. When this flag is set to true
, Authlete rejects dynamic client registration requests containing a registered software_id
. We ask Brazilian customers to set this flag to true
. In the Authlete web console, the item “DCR with Duplicate Software ID” corresponds to this flag.
Want to support local rules related to request object encryption in a generic way
In Brazil’s API ecosystem, the following local rules apply to request object encryption:
- When sending a request object via the front channel, it must be encrypted.
- The algorithm specified in the
alg
parameter of the encrypted request object must be a specific value. - The algorithm specified in the
enc
parameter of the encrypted request object must be a specific value.
In order to support these local rules without branching Authlete’s source code for Brazil, the following boolean flags were introduced:
Service.frontChannelRequestObjectEncryptionRequired
Service.requestObjectEncryptionAlgMatchRequired
Service.requestObjectEncryptionEncMatchRequired
In Authlete’s web console, these flags correspond to “Encryption in Front Channel,” “Encryption Algorithm Match,” and “Encryption Encoding Algorithm Match,” respectively.
OIDC DynReg defines the client metadata parameters request_object_encryption_alg
and request_object_encryption_enc
. These parameters allow a client application to declare the algorithms it intends to use for encrypting request objects by specifying values for alg
and enc
. However, according to their definitions, clients are not restricted to using only the declared algorithms—they are allowed to use others as well. Frankly, the actual benefit of these metadata parameters is unclear.
Since their original purpose holds little value, I decided to repurpose these metadata parameters for enforcing fixed encryption algorithms in our implementation.
When “Encryption Algorithm Match” and “Encryption Encoding Algorithm Match” are enabled, Authlete verifies that the alg
and enc
values in the encrypted request object match the values specified in these client metadata parameters.
The day has finally come when we are asked to reissue an ID token in the refresh token flow
As far as I can tell from reading Section 12.2. Successful Refresh Response of OIDC Core, reissuing an ID token in the refresh token flow is an optional feature. Since the implementation is cumbersome, Authlete had not supported ID token reissuance in the refresh token flow.
However, a customer has finally requested ID token reissuance in the refresh token flow. So, we finally decided to implement this feature.
Due to Authlete’s unique architecture, it does not store user information in its own database. Therefore, when generating an ID token, the user information to be embedded must be provided externally — that is, included in the Authlete API call. If we implement ID token reissuance in the refresh token flow, what used to be a single API call (just one call to Authlete’s /auth/token
API to complete the refresh token flow) will now require the token endpoint implementation to additionally call another Authlete API to provide user information.
That said, this two-step processing is not entirely new — we had already implemented a similar approach when supporting the resource owner password credentials (ROPC) flow. In the case of the ROPC flow, the JSON response from Authlete’s /auth/token
API includes "action": "PASSWORD"
. This allows the token endpoint implementation to recognize that the token request is using the ROPC flow, authenticate the user locally (by verifying the username
and password
included in the token request), and then call either the /auth/token/issue
API or the /auth/token/fail
API based on the authentication result.
We applied the same approach to ID token reissuance.
First, we implemented the /idtoken/reissue
API for ID token reissuance. Then, we introduced a new action value, ID_TOKEN_REISSUABLE
, in the response from the /auth/token
API.
The ID token reissuance feature has been implemented. However, if the /auth/token
API were to always return "action": "ID_TOKEN_REISSUABLE"
whenever ID token reissuance is possible, it would cause the same issue described in “The case of 401 Unauthorized from the Client Configuration Endpoint was overlooked.”
In other words, if the token endpoint implementation is not ready for receiving the value ID_TOKEN_REISSUABLE
as shown below (a case
for ID_TOKEN_REISSUABLE
is missing),
// Dispatch according to the action.
switch (action)
{
case INVALID_CLIENT:
// 401 Unauthorized
return ResponseUtil.unauthorized(content, CHALLENGE, headers);
case INTERNAL_SERVER_ERROR:
// 500 Internal Server Error
return ResponseUtil.internalServerError(content, headers);
case BAD_REQUEST:
// 400 Bad Request
return ResponseUtil.badRequest(content, headers);
case PASSWORD:
// Process the token request whose flow is "Resource Owner Password Credentials".
return handlePassword(response, headers);
case OK:
// 200 OK
return ResponseUtil.ok(content, headers);
case TOKEN_EXCHANGE:
// Process the token exchange request (RFC 8693)
return handleTokenExchange(response, headers);
case JWT_BEARER:
// Process the token request which uses the grant type
// urn:ietf:params:oauth:grant-type:jwt-bearer (RFC 7523).
return handleJwtBearer(response, headers);
default:
// This never happens.
throw getApiCaller().unknownAction("/api/auth/token", action);
}
the implementation would result in an error if Authlete’s /auth/token
API returns ID_TOKEN_REISSUABLE
.
Authlete should only return ID_TOKEN_REISSUABLE
when the token endpoint implementation is prepared to handle it. Specifically, this should be done after completing the work of adding a case
like the following to the switch
statement.
case ID_TOKEN_REISSUABLE:
// The flow of the token request is the refresh token flow
// and an ID token can be reissued.
return handleIdTokenReissuable(response, headers);
To control whether the /auth/token
API returns ID_TOKEN_REISSUABLE
, we introduced a boolean flag called Service.idTokenReissuable
. When this flag is set to true
, the /auth/token
API will return "action": "ID_TOKEN_REISSUABLE"
if the following conditions are all met:
- The request is for a refresh token flow.
- The scope in the token request after processing includes
openid
(Note: there can be cases where the scope is narrowed down). - The access token is associated with a user (Note: in the client credentials flow, the access token is not associated with a user).
- The access token is associated with the client application (Note: in the special case of the OID4VCI pre-authorized code flow, an access token not associated with any client application may be issued).
In the Authlete web console, the setting item “Enable Reissuable” corresponds to this flag.
Want to protect the Authlete APIs with access tokens instead of API key and secret pairs
Authlete provides a server-side implementation of OAuth 2.0, but in versions up to and including 2.3, the Authlete APIs themself are protected by a combination of API key and API secret, rather than an access token. This design was based on other web APIs referenced during the initial development of Authlete (in 2014).
However, protection using API key and secret pairs lacks flexibility compared to access tokens. Additionally, because Authlete does not issue multiple API key and secret pairs for each API user, multiple users have to share the same API key and secret.
Furthermore, Authlete versions up to 2.3 have two separate web consoles, which is confusing. Also, the login ID and password used to log into the web consoles are shared among multiple administrators, just like the API key and secret, leading to another problem.
To address these issues all at once, Authlete started developing Authlete 3.0. However, the development of Authlete 3.0 took longer than expected. According to records, the change to an access token-based API call method was made on October 25, 2021, in the Authlete 3.0 development branch. This was actually 3 years ago.
However, at long last, we were able to release Authlete 3.0 in November 2024! (announcement)
Access to the Authlete APIs and web console is controlled using access tokens. Fine-grained access control by the access tokens is described not by scopes, but by RAR (RFC 9396: OAuth 2.0 Rich Authorization Requests). The web console’s content is dynamically adjusted based on the access token’s permissions, which allowed us to consolidate the functionalities of two older web consoles into a single new web console. Additionally, by supporting login with external IdPs, account management has become more flexible. We also welcomed UI/UX experts to the team, greatly enhancing the appearance of the web console.
Finally
We started in 2014 with the fundamental specifications, RFC 6749 and OIDC Core, and continued implementing new specifications one after another.
In the summer of 2018, we released Authlete 2.0 with support for FAPI 1.0. In 2023, a major U.S. vendor announced its FAPI support, meaning we were five years ahead.
By the end of 2018, we had completed the implementation of CIBA. Around this time, we realized that we had become “the fastest vendor in the world to implement new specifications.”
In November 2024, we released Authlete 3.0. In addition to standard specification implementations, Authlete 3.0 also incorporates the knowledge and expertise gained from a decade of commercial operation.
Please try out Authlete 3.0! → https://console.authlete.com/
For inquiries, contact us here! → https://www.authlete.com/ja/contact/