Full-Scratch Implementor of OAuth and OpenID Connect Talks About Findings

1. Introduction

In this post, a developer who has implemented an OAuth 2.0 and OpenID Connect server from scratch (me) talks about findings. Basically, consideration points for implementation are written discursively. Therefore, this is not a document for those who are looking for information like “How to set up an OAuth 2.0 and OpenID Connect server promptly”. If you are looking for such information, please visit java-oauth-server and java-resource-server on GitHub. Using these, you can start an authorization server and a resource server, issue an access token and call a Web API with the access token, in 10 minutes with no need to set up your DB server.

Bias

I’m a co-founder of Authlete, Inc. which is a company providing implementations of OAuth 2.0 and OpenID Connect on cloud, so this document may be affected by such a biased standpoint. Therefore, read this document with it in your mind. However, basically, I’m going to write this post from a viewpoint of a pure engineer.

2. Is OAuth Necessary?

“We want to do this and that on our corporate website. Should we implement OAuth?” — This is often asked. In essence, this question is asking what OAuth is.

  1. You want third-parties to develop applications for users of your service.
  2. You don’t want to reveal credentials of users to applications developed by third-parties.

3. Authentication and Authorization

I explain the term that makes people confused — “OAuth authentication”.

  • Authorization — Who grants what permissions to whom.
(Identity, Authentication) + OAuth 2.0 = OpenID Connect
“Say OAuth is an Authentication standard again.” by Mr. Nat Sakimura and Mr. John Bradley. (from https://twitter.com/ve7jtb/status/740650395735871488)

4. Relationship between OAuth 2.0 and OpenID Connect

All the contents so far are, however, just a preamble of this post. Technical contents for developers start from here. The first topic is the relationship between OAuth 2.0 and OpenID Connect.

5. Response Type

Especially, what conflicts with an existing implementation is the way to process the request parameter, response_type. It is sure that RFC 6749 states the request parameter may take multiple values, but it is just a possibility in the future. If we read RFC 6749 straightforwardly, response_type is either code or token. It is almost impossible to imagine that these two are set at the same time. It is because the parameter is used to determine the flow to process a request from a client application. To be concrete, the authorization code flow is used when the value of response_type is code, and the implicit flow is used when the value is token. Who can imagine these flows are mixed? Even if one could imagine it, how should we resolve conflicts that exist between the flows? For example, the authorization code flow requires that response parameters be embedded in the query part of a redirect URI (4.1.2. Authorization Response) while the implicit flow requires that response parameters be embedded in the fragment part (4.2.2. Access Token Response), and these requirements cannot be satisfied simultaneously.

6. Metadata of Client Application

As written explicitly in 2. Client Registration of RFC 6749, a client application has to be registered into the target authorization server in advance before it makes an authorization request. Therefore, in a typical case, an implementor of an authorization server defines a database table to store information about client applications.

  1. Client Secret
  2. Client Type
  3. Redirect URIs
  1. response_typesresponse_type values that the Client is declaring that it will restrict itself to using.
  2. grant_types — Grant Types that the Client is declaring that it will restrict itself to using.
  3. application_type — Kind of the application.
  4. contacts — e-mail addresses of people responsible for this Client.
  5. client_name — Name of the Client to be presented to the End-User.
  6. logo_uri — URL that references a logo for the Client application.
  7. client_uri — URL of the home page of the Client.
  8. policy_uri— URL that the Relying Party Client provides to the End-User to read about how the profile data will be used.
  9. tos_uri— URL that the Relying Party Client provides to the End-User to read about the Relying Party’s terms of service.
  10. jwks_uri— URL for the Client’s JSON Web Key Set document.
  11. jwks — Client’s JSON Web Key Set document, passed by value.
  12. sector_identifier_uri — URL using the https scheme to be used in calculating Pseudonymous Identifiers by the OP.
  13. subject_typesubject_type requested for responses to this Client.
  14. id_token_signed_response_alg — JWS alg algorithm required for signing the ID Token issued to this Client.
  15. id_token_encrypted_response_alg — JWE alg algorithm required for encrypting the ID Token issued to this Client.
  16. id_token_encrypted_response_enc— JWE enc algorithm required for encrypting the ID Token issued to this Client.
  17. userinfo_signed_response_alg— JWS alg algorithm required for signing UserInfo Responses.
  18. userinfo_encrypted_response_alg — JWE alg algorithm required for encrypting UserInfo Responses.
  19. userinfo_encrypted_response_enc — JWE enc algorithm required for encrypting UserInfo Responses.
  20. request_object_signing_response_alg — JWS alg algorithm that must be used for signing Request Objects sent to the OP.
  21. request_object_encryption_alg — JWE alg algorithm that RP is declaring that it may use for encrypting Request Object sent to the OP.
  22. request_object_encryption_enc — JWE enc algorithm the RP is declaring that it may use for encrypting Request Objects sent to the OP.
  23. token_endpoint_auth_method— Requested Client Authentication method for the Token Endpoint.
  24. token_endpoint_auth_signing_alg — JWS alg algorithm that must be used for signing the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods.
  25. default_max_age — Default Maximum Authentication Age.
  26. require_auth_time — Boolean value specifying whether the auth_time Claim in the ID Token is required.
  27. default_acr_values — Default requested Authentication Context Class Reference values.
  28. initiate_login_uri — URI using the https scheme that a third party can use to initiate a login by the RP.
  29. request_uris request_uri values that are pre-registered by the RP for use at the OP.

6.1. Client Type

I’m afraid it is a kind of a mistake in defining the specification that 2. Client Metadata of OpenID Connect Dynamic Client Registration 1.0 does not contain “client type”. The reason I think so is that the difference between the two client types, “confidential” and “public” (which are defined in 2.1. Client Types of RFC 6749), must be taken into consideration when we implement an authorization server. As a matter of fact, “client type” is listed as an example of client properties to be registered in 2. Client Registration of RFC 6749 as follows.

6.2. Application Type

According to the specification, application_type is an optional attribute. Pre-defined values for application_type are native and web. If omitted, web is used as the default value.

6.3. Client Secret

How long should the length of client secret be?

GBAyfVL7YWtP6gudLIjbRZV_N0dW4f3xETiIxqtokEAZ6FAsBtgyIq0MpU1uQ7J08xOTO2zwP0OuO3pMVAUTid

6.4. Signature Algorithm

id_token_signed_response_alg is listed in “2. Client Metadata” of OpenID Connect Dynamic Client Registration 1.0. It denotes the algorithm that a client application requires the authorization server to use as a signature algorithm for ID tokens. Valid values are listed in RFC 7518 as mentioned above, and it should be noted that none is not allowed. If the value of id_token_signed_response_alg is omitted on registration, RS256 is used.

6.5. Client Application Developer

Some open-source authorization servers provide a mechanism to enable dynamic registration of client applications such an HTML form (OpenAM by ForgeRock) and Web APIs (MITREid Connect by MITRE). But, it seems only administrators of authorization servers can register client applications. However, an ideal approach will be to create something similar to Twitter’s Application Management console, let developers login there, and provide an environment in which each developer can register and manage his/her own client applications. To achieve this, a database table for client applications should have a column which holds developers’ unique identifiers.

7. Access Token

7.1. Access Token Representation

How should an access token be represented? There are two major ways.

  1. As a self-contained string which is a result of encoding access token information by base64url or something similar.

7.2. Access Token Deletion

To prevent a database from growing infinitely, expired access tokens should be deleted from the database periodically.

8. Redirect URI

8.1. Redirect URI Validation

In May, 2014, a Ph.D. student in Singapore posted an article, and it made people buzz about “Vulnerability in OAuth?” It is an issue about so-called Covert Redirect. Those who understand OAuth 2.0 correctly soon realized that it was not due to vulnerability in the specification but just due to the improper implementations. However, the topic made so many people upset that experts in the OAuth area could not help writing explanatory documents. Covert Redirect and its real impact on OAuth and OpenID Connect by Mr. John Bradley is one of such documents.

// Extract the value of the 'redirect_uri' parameter from
// the authorization request.
redirectUri = ...
// Remember whether a redirect URI was explicitly given.
// It must be checked later in the implementation of the
// token endpoint because RFC 6749 states as follows.
//
// redirect_uri
// REQUIRED, if the "redirect_uri" parameter was
// included in the authorization request as described
// in Section 4.1.1, and their values MUST be identical.
//
explicit = (redirectUri != null);
// Extract registered redirect URIs from the database.
registeredRedirectUris = ...
// Requirements by RFC 6749 (OAuth 2.0) and those by
// OpenID Connect are different. Therefore, the code flow
// branches according to whether the request is an OpenID
// Connect request or not. This is judged by whether the
// 'scope' request parameter contains 'openid' as a value.
if ( 'openid' is included in 'scope' )
{
// Check requirements by OpenID Connect.
// If the 'redirect_uri' is not contained in the request.
if ( redirectUri == null )
{
// The 'redirect_uri' parameter is mandatory in
// OpenID Connect. It's optional in RFC 6749.
throw new Exception(
"The 'redirect_uri' parameter is missing.");
}
// For each registered redirect URI.
for ( registeredRedirectUri : registeredRedirectUris )
{
// 'Simple String Comparison' is required by the
// specification.
if ( registeredRedirectUri.equals( redirectUri ) )
{
// OK. The redirect URI specified by the
// authorization request is registered.
registered = true;
break;
}
}
// If the redirect URI specified by the authorization
// request matches none of the registered redirect URIs.
if ( registered == false )
{
throw new Exception(
"The redirect URI is not registered.");
}
}
else
{
// Check requirements by RFC 6749.
// If redirect URIs are not registered at all.
if ( registeredRedirectUris.size() == 0 )
{
// RFC 6749, 3.1.2.2. Registration Requirements says
// as follows:
//
// The authorization server MUST require the
// following clients to register their
// redirection endpoint:
//
// o Public clients.
// o Confidential clients utilizing the
// implicit grant type.
// If the type of the client application which made
// the authorization request is 'public'.
if ( client.getClientType() == PUBLIC )
{
throw new Exception(
"A redirect URI must be registered.");
}
// If the client type is 'confidential' and if the
// authorization flow is 'Implicit Flow'. If the
// 'response_type' request parameter contains either
// or both of 'token' and 'id_token', the flow should
// be treated as a kind of 'Implicit Flow'.
else if ( responseType.requiresImplicitFlow() )
{
throw new Exception(
"A redirect URI must be registered.");
}
}
// If the authorization request does not contain the
// 'redirect_uri' request parameter.
if ( redirectUri == null )
{
// If redirect URIs are not registered at all,
// or if multiple redirect URIs are registered.
if ( registeredRedirectUris.size() != 1 )
{
// A redirect URI must be explicitly specified
// by the 'redirect_uri' parameter.
throw new Exception(
"The 'redirect_uri' parameter is missing.");
}
// One redirect URI is registered. Use it as the
// default value of redirect URI.
redirectUri = registeredRedirectUris[0];
}
// The authorization request contains the 'redirect_uri'
// parameter, but redirect URIs are not registered.
else if ( registeredRedirectUris.size() == 0 )
{
// The code flow reaches here if and only if the
// client type is 'confidential' and the authorization
// flow is not 'Implicit Flow'. In this case, the
// redirect URI specified by the 'redirect_uri'
// parameter of the authorization request is used
// although it is not registered. However,
// requirements written in RFC 6749, 3.1.2.
// Redirection Endpoint are checked.
// If the specified redirect URI is not an absolute one.
if ( redirectUri.isAbsolute() == false )
{
throw new Exception(
"The 'redirect_uri' is not an absolute URI.");
}
// If the specified redirect URI has a fragment part.
if ( redirectUri.getFragment() != null )
{
throw new Exception(
"The 'redirect_uri' has a fragment part.");
}
}
else
{
// If the specified redirect URI is not an absolute one.
if ( redirectUri.isAbsolute() == false )
{
throw new Exception(
"The 'redirect_uri' is not an absolute URI.");
}
// If the specified redirect URI has a fragment part.
if ( redirectUri.getFragment() != null )
{
throw new Exception(
"The 'redirect_uri' has a fragment part.");
}
// For each registered redirect URI.
for (registeredRedirectUri : registeredRedirectUris )
{
// If the registered redirect URI is a full URI.
if ( registeredRedirectUri.getQuery() != null )
{
// 'Simple String Comparison'
if ( registeredRedirectUri.equals( redirectUri ) )
{
// The specified redirect URI is registered.
registered = true;
break;
}
// This registered redirect URI does not match.
continue;
}
// Compare the scheme parts.
if ( registeredRedirectUri.getScheme().equals(
redirectUri.getScheme() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// Compare the user information parts. Here I use
// an imaginary method 'equalsSafely()' because
// the code would become too long if I inlined it.
// The method compares arguments without throwing
// any exception even if either or both of the
// arguments are null.
if ( equalsSafely(
registeredRedirectUri.getUserInfo(),
redirectUri.getUserInfo() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// Compare the host parts. Ignore case sensitivity.
if ( registeredRedirectUri.getHost().equalsIgnoreCase(
redirectUri.getHost() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// Compare the port parts. Here I use an imaginary
// method 'getPortOrDefaultPort()' because the
// code would become too long if I inlined it. The
// method returns the default port number of the
// scheme when 'getPort()' returns -1. The last
// resort is 'URI.toURL().getDefaultPort()'. -1 is
// returned If 'getDefaultPort()' throws an exception.
if ( getPortOrDefaultPort( registeredRedirectUri ) !=
getPortOrDefaultPort( redirectUri ) )
{
// This registered redirect URI does not match.
continue;
}
// Compare the path parts. Here I use the imaginary
// method 'equalsSafely()' again.
if ( equalsSafely( registeredRedirectUri.getPath(),
redirectUri.getPath() ) == false )
{
// This registered redirect URI does not match.
continue;
}
// The specified redirect URI is registered.
registered = true;
break;
}
// If none of the registered redirect URI matches.
if ( registered == false )
{
throw new Exception(
"The redirect URI is not registered.");
}
}
}
// Check requirements by the 'application_type' of the client.// If the value of the 'application_type' attribute is 'web'.
if ( client.getApplicationType() == WEB )
{
// If the authorization flow is 'Implicit Flow'. When the
// 'response_type' request parameter of the authorization
// request contains either or both of 'token' and 'id_token',
// it should be treated as a kind of 'Implicit Flow'.
if ( responseType.requiresImplicitFlow() )
{
// If the scheme of the redirect URI is not 'https'.
if ( "https".equals( redirectUri.getScheme() ) == false )
{
// The scheme part of the redirect URI must be
// 'https' when a client application whose
// 'application_type' is 'web' uses 'Implicit Flow'.
throw new Exception(
"The scheme of the redirect URI is not 'https'.");
}
// If the host of the redirect URI is 'localhost'.
if ( "localhost".equals( redirectUri.getHost() ) )
{
// The host of the redirect URI must not be
// 'localhost' when a client application whose
// 'application_type' is 'web' uses 'Implicit Flow'.
throw new Exception(
"The host of the redirect URI is 'localhost'.");
}
}
}
// If the value of the 'application_type' attribute is 'native'.
else if ( client.getApplicationType() == NATIVE )
{
// If the scheme of the redirect URI is 'https'.
if ( "https".equals( redirectUri.getScheme() ) )
{
// The scheme of the redirect URI must not be 'https'
// when the 'application_type' of the client is 'native'.
throw new Exception(
"The scheme of the redirect URI is 'https'.");
}
// If the scheme of the redirect URI is 'http'.
if ( "http".equals( redirectUri.getScheme() ) )
{
// If the host of the redirect URI is not 'localhost'.
if ( "localhost".equals(
redirectUri.getHost() ) == false )
{
// When a client application whose 'application_type'
// is 'native' uses a redirect URI whose scheme is
// 'http', the host port of the URI must be
// 'localhost'.
throw new Exception(
"The host of the redirect URI is not 'localhost'.");
}
}
}
// If the value of the 'application_type' attribute is neither
// 'web' or 'native'.
else
{
// As mentioned above, Authlete allows 'unspecified' as a
// value of the 'application_type' attribute. Therefore,
// no exception is thrown here.
}

8.2. Other’s implementation

In OpenID Connect, the redirect_uri parameter is mandatory and requirements about how to check whether a presented redirect URI is registered or not are just ‘Simple String Comparison’. Therefore, if what you have to care about is OpenID Connect only, implementations will be simple. For example, in IdentityServer3 which has won about 1,700 stars on GitHub as of October, 2016 and been certified by OpenID Certification program, checking a redirect URI is implemented as follows (excerpt from DefaultRedirectUriValidator.cs with additional newlines for formatting).

public virtual Task<bool> IsRedirectUriValidAsync(
string requestedUri, Client client)
{
return Task.FromResult(
StringCollectionContainsString(
client.RedirectUris, requestedUri));
}
if (request.RequestedScopes.Contains(
Constants.StandardScopes.OpenId))
{
request.IsOpenIdRequest = true;
}

//////////////////////////////////////////////////////////
// check scope vs response_type plausability
//////////////////////////////////////////////////////////
var requirement =
Constants.ResponseTypeToScopeRequirement[request.ResponseType];
if (requirement == Constants.ScopeRequirement.Identity ||
requirement == Constants.ScopeRequirement.IdentityOnly)
{
if (request.IsOpenIdRequest == false)
{
LogError("response_type requires the openid scope", request);
return Invalid(request, ErrorTypes.Client);
}
}
//////////////////////////////////////////////////////////
// redirect_uri must be present, and a valid uri
//////////////////////////////////////////////////////////
var redirectUri = request.Raw.Get(Constants.AuthorizeRequest.RedirectUri);

if (redirectUri.IsMissingOrTooLong(
_options.InputLengthRestrictions.RedirectUri))
{
LogError("redirect_uri is missing or too long", request);
return Invalid(request);
}

9. Violations of Specifications

Subtle violations of the specifications are sometimes called “dialects”. The word “dialect” may give an impression of “acceptable”, but violations are violations. If there are no dialects, it will be enough to have one generic OAuth 2.0 / OpenID Connect library for each computer language. But, in the real world, custom client libraries are needed for authorization servers which violate the specifications.

9.1. Delimiter of Scope List

Scope names are listed in the scope parameter of requests to an authorization endpoint and a token endpoint. RFC 6749, 3.3. Access Token Scope requires that spaces be used as delimiters, but the following OAuth implementations use commas:

  • GitHub
  • Spotify
  • Discus
  • Todoist

9.2. Response Format of Token Endpoint

RFC 6749, 5.1. Successful Response requires that the format of a successful response from a token endpoint be JSON, but the following OAuth implementations use application/x-www-form-urlencoded:

  • Bitly
  • GitHub

9.3. token_type in Response from Token Endpoint

RFC 6749, 5.1. Successful Response requires that the token_type parameter be included in a successful response from a token endpoint, but the following OAuth implementation does not include it:

9.4. token_type Inconsistency

The following OAuth implementation claims that the token type is “Bearer”, but its resource endpoints do not accept an access token by the means defined in RFC 6750 (The OAuth 2.0 Authorization Framework: Bearer Token Usage):

9.5. grant_type Is Not Required

The grant_type parameter is mandatory at a token endpoint, but the following OAuth implementations don’t require it:

  • Slack
  • Todoist

9.6. Unofficial Values for The error Parameter

The specifications have defined some values for the error parameter which is included in an error response from an authorization server, but the following OAuth implementations define their own:

  • Todoist (e.g. bad_authorization_code)

9.7. Bad Parameter Name on Error

The following OAuth implementation uses errorCode instead of error when it returns an error code:

10. Proof Key for Code Exchange

10.1. PKCE Is A MUST

Do you know PKCE? It is a specification defined as RFC 7636 (Proof Key for Code Exchange by OAuth Public Clients) and was published in September, 2015. It is a countermeasure against the authorization code interception attack.

10.2. Server-Side Implementation

In the implementation of an authorization endpoint, what an authorization server has to do is to save the values of the code_challenge parameter and the code_challenge_method parameter contained in an authorization request into the database. So, there is nothing interesting in the implementation code. Something to note is just that an authorization server which wants to support PKCE has to add columns for code_challenge and code_challenge_method into the database table storing authorization codes.

private void validatePKCE(AuthorizationCodeEntity acEntity)
{
// See RFC 7636 (Proof Key for Code Exchange) for details.

// Get the value of 'code_challenge' which was contained in
// the authorization request.
String challenge = acEntity.getCodeChallenge();

if (challenge == null)
{
// The authorization request did not contain
// 'code_challenge'.
return;
}

// If the authorization request contained 'code_challenge',
// the token request must contain 'code_verifier'. Extract
// the value of 'code_verifier' from the token request.
String verifier = extractFromParameters(
"code_verifier", invalid_grant, A050312, A050313, A050314);

// Compute the challenge using the verifier
String computedChallenge = computeChallenge(acEntity, verifier);

if (challenge.equals(computedChallenge))
{
// OK. The presented code_verifier is valid.
return;
}

// The code challenge value computed with 'code_verifier'
// is different from 'code_challenge' contained in the
// authorization request.
throw toException(invalid_grant, A050315);
}


private String computeChallenge(
AuthorizationCodeEntity acEntity, String verifier)
{
CodeChallengeMethod method = acEntity.getCodeChallengeMethod();

// This should not happen, but just in case.
if (method == null)
{
// Use 'plain' as the default value required by RFC 7636.
method = CodeChallengeMethod.PLAIN;
}

switch (method)
{
case PLAIN:
// code_verifier
return verifier;

case S256:
// BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
return computeChallengeS256(verifier);

default:
// The value of code_challenge_method extracted
// from the database is not supported.
throw toException(server_error, A050102);
}
}


private String computeChallengeS256(String verifier)
{
// BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

// SHA256
byte[] hash =
Digest.getInstanceSHA256().update(verifier).digest();

// BASE64URL
return SecurityUtils.encode(hash);
}
byte[] hash = Digest.getInstanceSHA256().update(verifier).digest();

10.3. Client-Side Implementation

What a client application has to do for PKCE are two. One is to generate a random code verifier which consists of 43–128 letters, compute the code challenge using the code verifier and the code challenge method (plain or S256), and include the computed code challenge and the code challenge method as the values of the code_challenge parameter and the code_challenge_method parameter in an authorization request. The other is to include the code verifier in a token request.

  1. AppAuth for iOS

11. Finally

Some may say it is easy to implement OAuth and OpenID Connect, and others may say it’s not. In either case, as a matter of fact, even big tech companies such as Facebook and GitHub that have sufficient budget and human resources have failed to implement OAuth and OpenID Connect correctly. Famous open-source projects such as Apache Oltu and Spring Security have problems, too. Therefore, if you implement OAuth and OpenID Connect by yourself, take it seriously and prepare a decent development team. Otherwise, security risks would be increased.

Co-founder and representative director of Authlete, Inc., working as a software engineer since 1997. https://www.authlete.com/

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store