Personal Website of Levi Carter - Senior Software Developer with Marketpath from Noblesville, Indiana.
Read About Me
Close

IdentityServer, OAuth, and OpenIDConnect

As we begin the shift from a "hybrid-monolith" application to a "microservices" application, one of the first questions we face is how to handle authentication and authorization. In our monolith it was comparatively easy to implement our own secure authentication and authorization mechanisms, but the security demands of a distributed application are much more rigorous.

Rather than trying to retro-fit our existing solution into a new architecture or re-engineer an entirely new solution on our own, it seemed good to us to learn more about broader industry practices and solutions that other people have refined over the last decade and see what we are able to relatively easily adopt that will provide greater long-term reliability, compatability, security, and functionality with less effort and maintenance on our part.

Much of this "learning" is actually information that we were already aware of and at least tangentially familiar with. After all, how many web developers are still unaware of the term "OAuth"? But if we were going to implement and rely on these technologies, we would need intimiate knowlege of how they work as well as all of the various technologies that they interface with.

Here is a high-level overview of some of those technologies:

OAuth 2.0

OAuth stands for "Open Authorization". From the RFC: OAuth 2.0 is an authorization framework that enables third-party applications to obtain limited access to an HTTP service. It introduces an authorization layer and separates the role of the client from that of the resource owner. In OAuth, the (third-party) client requests access to resources controlled by the resource owner and hosted by the resource server, and is issued an access token denoting a specific scope, lifetime, and other access attributes. Access tokens are issued by an authorization server with the approval of the resource owner, and may be used to access the protected resources hosted by the resource server.

To verify authorization, resource servers must validate the access token and ensure that its scope covers the requested resource. Typically this requires an interaction or coordination between the resource server and the authorization server but may be accomplished by another means instead (according to the RFC).

Note that OAuth 2.0 is specifically designed to authenticate "clients" with an authorization server, and does NOT deal with "end user" authentication.

Terms

Client: The application requesting authorization from the authorization server. May be a browser, API Server, or any other application. There are two client types: Confidential clients are capable of maintaining the confidentiality of their credentials or capable of secure client authentication using other means. Public clients are incapable of maintaining the confidentiality of their credentials such as clients executing on the device used by the resource owner.

Resource Owner: The end-user who "owns" the "resource". The most basic "resource" might be the user's email address. In Marketpath CMS the "resource" is a website (and "owning" simply means having access to).

Resource Server: The server that directly accesses any protected resources (eg: the Rest and WebSocket API Servers).

Authorization Server: The server that authenticates end users and obtains authorization for the client to access that user's resources.

Authorization Grant: A credential representing the resource owner's authorization to access its protected resources, used by the client to obtain an access token. The four authorization grant types are: authorization code, implicit, resource owner password credentials, and client credentials. There is also an extensibility mechanism for defining additional types.

  • Authorization Code: Obtained by using an authorization server as an intermediary between the client and resource owner using the following flow: 1) The client directs the resource owner to an authorization server. 2) The authorization server authenticates the resource owner and obtains authorization from them to grant the requested access to the client. 3) The authorization server directs the resource owner back to the client with the authorization code. Note that the authorization code is just an object used to fetch the access token from the authorization server after authorization.
  • Implicit Grant: A simplified authorization code flow optimized for clients implemented in a browser using a scripting language such as JavaScript. Instead of issuing the client an authorization code, the client is issued an access token directly. When issuing an access token during the implicit grant flow, the authorization server does not authenticate the client. Implicit grants improve the responsiveness and efficiency of some clients since it reduces the number of round trips required to obtain an access token but should be weighed against the security implications of using implicit grants.
  • Resource Owner Password Credentials: The resource owner password credentials can be used directly as an authorization grant to obtain an access token. Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used for a single request and are exchanged for an access token. Not supported by OpenID Connect.
  • Client Credentials: The client credentials (or other forms of client authentication) can be used as an authorization grant when the authorization scope is limited to the protected resources under the control of the client, or to protected resources previously arranged with the authorization server. Not supported by OpenID Connect.

Access Token: Access tokens are credentials used to access protected resources. An access token is a string representing an authorization issued to the client with specific scopes and durations of access. The token may denote an identifier used to retrieve the authorization information or may self-contain the authorization information. Access tokens may be more restrictive than the authorization grant used to obtain them.

Refresh Token: Refresh tokens are credentials issued to the client by the authorization server which are used to obtain a new access token when the current access token becomes invalid or expires or to obtain additional access tokens with identical or narrower scope. Issuing a refresh token is optional at the discretion of the authorization server.  If the authorization server issues a refresh token, it is included when issuing an access token.

OAuth 2.0 Bearer Token

The RFC describes how to make protected resource requests when the OAuth access token is a bearer token. Any party in possession of a bearer token (a "bearer") can use it to get access to the associated resources.

The bearer token may be sent in several ways, but the primary recommended way is using the "Authorization" request header as follows: "Authorization: Bearer b64token". If the resource server is unable to use the access token to authorize access to the protected resource then it must respond with the "WWW-Authenticate" response header.

Bearer Token: A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession). To limit the attack surface related to bearer tokens, they should be issued with as narrow a scope and as specific of an audience as possible (eg: only allow access to a specific resource from a specific server vs allow access to any resource from any server).

OpenID Connect

While OAuth defines how authentication should be handled, it does NOT define how authorization or identity should be defined. This is where OpenID Connect comes in.

The Open ID specification is defined in multiple documents, linked to from the OpenID website.

OpenID Connect defines a framework by which a client may verify an end-user's identity (as defined by the authorization server). It is designed to be used in conjunction with and to extend OAuth 2.0. Authentication servers that support OpenID Connect are referred to as OpenID Providers while clients using OpenID Connect are referred to as Relying Parties.

The following parameters may be sent with the authentication request:

  • scope: must contain the "openid" scope value along with any others.
  • response_type: "code" for authorization code flow; "id_token" or "id_token token" for implicit flow; "code id_token", "code token", or "code id_token token" for hybrid flow.
  • client_id: identifier of the client the authentication request was sent from.
  • redirect_uri: The URI where the authorization response will be sent (which will be verified by the authorization server)
  • state: (recommended) Opaque value used to maintain state bewteen the request and the callback.
  • nonce: Pseudo-random string value used to associate a Client session with an ID Token, and to mitigate replay attacks. Optional if using the authorization code flow, required if using the implicit flow.
  • display: (optional) Value that specifies how the authorization server should display the authentication and consent pages to the end-user:
    • page: Full page view (default)
    • popup: Display in a popup window of an appropriate size for a long-fixued dialog which does not obscure the entire window that it is popping up over
    • touch: Use a UI for a device that leverages a touch interface
    • wap: Display the UI consistent with a "feature phone" type display.
  • prompt: (optional) List of values that specify whether the authorization server prompts the end-user for reauthentication and consent:
    • none: Do not display any authentication or consent pages. Used to check for existing authentication and/or consent.
    • login: Prompt the end-user for reauthentication, even if they are already authenticated
    • consent: Prompt the end-user for consent, even if they have already granted it
    • select_account: Prompt the end-user to select a user account. Used for end-users who might have multiple accounts at the authorization server.
  • max_age: (optional) Maximum allowable time in seconds since the last time the end-user was actively authenticated by the OpenID Provider.
  • ui_locales: (optional) End-user's preferred languages and scripts for the user interface, ordered by preference.
  • id_token_hint: (optional) Token previously issued by the authorization server being passed as a hint about the user's current or past authenticated session with the client.
  • login_hint: (optional) Hint to the authorization server about the user's login identifier (eg: if the client knows the users email address before sending the authentication request)
  • acr_values: (optional) Requested "Authentication Context Class Reference" (ie: Level of Assurance Profile) values with the values appearing in order of preference.
  • Other: Other values may be sent along witht he request as needed or desired by either the client or the authorization server.

Use of the OpenID Connect "extension" to OAuth is requested by clients by including the openid scope value in the authorization request. Information about the authentication performed is returned in a JSON Web Token (JWT) called an ID Token. The ID Token includes the following claims:

  • iss: Https URL identifying the issuer of the response
  • sub: Subject Identifier. A unique identifier within the Issuer for the End-User
  • aud: Audience(s) that this ID Token is intended for. It must at a minimum contain the OAuth 2.0 client_id of the Relying Party.
  • exp: Expiration time for the ID Token, represented as a UNIX timestamp.
  • iat: Time at which the JWT was issued, represented as a UNIX timestamp.
  • auth_time: Time when the actual end-user authentication occurre, represented as a UNIX timestamp. Optional unless a max_age request is made or auth_time is requested as an Essential Claim.
  • nonce: Contains the same value sent in the authentication request, if any, in order to associate a client session with an ID token and to prevent replay attacks.
  • acr: Optional string specifying an Authentication Context Class Reference value (either as a well-known name or as a URL) that identifies the Level of Assurance (LoA) Profile used to obtain end-user authentication. The value "0" essentially indicates a high level of insecurity in the method used to obtain authentication (eg: URL query parameters, cookie, long-lived token, etc...) and should NOT be used to authorize access to any resource of any monetary value.
  • amr: Optional list of the authentication methods used (eg: user, pin, mfa, fpt (fingerprint), sms, geo, etc...). The definition of particular values to be used in the amr may be defined by the authorization server.
  • azp:Optional identifier which must contain the OAuth 2.0 Client ID of the party to which the ID Token was issued. It is only needed when the ID Token has a single audience value which is different than the authorized party.
  • Other: The ID Token may contain other claims (eg: those requested by the client, etc...)

The following standard claims are defined by OpenID Connect

  • sub: Identifier for the end-user ("Subject"). There are two types of subject identifiers defined by the spec:
    • public: The same sub value is returned to all clients (default)
    • pairwise: A different sub value is returned to each client, which prevents clients from correlating the user's activities without permissions. Clients may be grouped together using a common "sector identifier", in which case the same sub value is returned to all clients using the same sector identifier (which is validated during client registration).
  • name, given_name, family_name, middle_name, nickname, gender
  • preferred_username: Shorthand name by which the user wishes to be referred to. May contain special characters and may not be unique.
  • profile: Url to the user's profile page (presumably public)
  • picture: Url of the user's profile picture
  • website: User's web page or blog
  • email: User's preferred email address. May not be unique.
  • email_verified: True if the email address has been verified.
  • birthdate: YYYY-MM-DD, 0000-MM-DD if the year has not been specified, or YYYY to only list the birth year
  • zoneinfo: The user's timezone (from the zoneinfo time zone database)
  • locale: The user's locale represented as a language tag (eg: en-US or fr_CA)
  • phone_number: The user's preferred telephone number (preferably in E.164 format with any extenion in FVC 3966 format)
  • phone_number_verified: True if the phone number has been verified
  • address: The user's preferred postal address as a JSON object with the following properties (all optional and may or may not be returned depending on the OpenID Provider's implementation, the user's preferences, the data available, or other factors):
    • formatted: Full mailing address formatted for display or use on a mailing label, which may contain multiple lines or may be all on line line.
    • street_address: Full street address component, which may optionally include the house number, street name, post office pox, and multi-line extended street address information.
    • locality: City of locality component
    • region: State, province, prefecture, or region component
    • postal_code: Zip code or postal code component
    • country: Country name component
  • updated_at: The unix timestamp that the user's information was last updated

The following standard scopes are used to request a specific set of standard claims:

  • profile: requests the name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and update_at claims
  • email: requests the email and email_verified claims
  • address: requests the address claim
  • phone: requests the phone_number and phone_number_verified claims

Additionally, the special scope value of offline_access may be used to request a refresh token that can be used to obtain an access token that grants access to the user's info even when the user is not logged in. It must be sent with a prompt value of consent, and the authentication server will prompt the user for consent even if it has already been previously granted.

When requesting claims, the client may specify if the individual claims are "essential" and/or if they should have a specific "value" or "values" (eg: to request a specific "subject" or "acr" value). The client should verify that the response from the authorization server match the requested and/or expected values keeping in mind that not all requested claims (even "essential" claims) may be returned.

UserInfo Endpoint

The UserInfo endpoint is a special protected resource that returns claims about the authenticated user.

Discovery

The short version is that the discovery endpoint allows "Relying Parties" to query for and learn information about OpenID Providers that may not be explicitly known or programmed into the client. It is entirely optional. If used, it reveals information about the endpoints and features implemented by various OpenID providers. I will not list all of the metadata it contains, which may be referenced in the OpenID Connect Discovery 1.0 spec.

OpenID Providers that support discovery make their discovery document available at https://[issuer]/.well-known/openid-configuration. See the Google OpenID Provider discovery document as an example.

Dynamic Client Registration

OpenID Providers can optionally enabled dynamic client registration - allowing previously unknown clients to rely on them for identity and authorization information as defined in the OpenID Connect Dynamic Client Registration 1.0 spec.

Clients may register and/or update their configuration using dynamic client registration endpoints. Some of the client metadata defined by the spec include:

  • redirect_uris: Required array of URIs used by the client for authorization requests.
  • response_types: The types of responses that the client will restrict itself to. Defaults to "code"
  • grant_types: The types of authorization grants that the client will restrict itself to. Defaults to "authorization_code"
  • application_type: The kind of the application. Defaults to "web". The other common kind is "native".
  • contacts: Optional array of e-mail addresses for people responsible for this client.
  • client_name: Optional name of the client to be presented to the end-user (multi-lingual compatible).
  • logo_uri: Optional URL that references a logo to be displayed to the end-user during approval (multi-lingual compatible).
  • client_uri: Optional URL of the home page of the client (multi-lingual compatible).
  • policy_uri: Optional URL of the public web page containing information about how the profile data will be used (multi-lingual compatible).
  • tos_uri: Optional URL of the public web page containing information about the client's terms of service (multi-lingual compatible).
  • sector_identifier_uri: Optional https URL used to calculate the pairwise subject identifier, which must reference all possible redirect_uris for all clients with the same sector_identifier_uri.
  • subject_type: Optional subject_type requested for responses to this client (pairwise/public).
  • Various signing algorithms and metadata (jwks_uri, jwks, id_token_signed_response_alg, id_token_encrypted_response_alg, userinfo_encrypted_response_alg, userinfo_encrypted_response_enc, request_object_signing_alg, request_object_encryption_alg, request_object_encryption_enc, token_endpoint_auth_method, token_endpoint_auth_signing_alg)
  • default_max_age: Optional default max_age for authentication requests that do not explicitly define this value.
  • require_auth_time: If true, the ID token must include the auth_time claim.
  • default_acr_values: Optional default acr_values for authentication requests that do not excplicitly include this paramter.
  • initiate_login_uri: Optional https URI that a third party can use to initiate a login by the "Relying Party".
  • request_uris: Optional array of request_uri values that are pre-registered by the "Relying Party" for use at the OpenId Provider.

In response to the client registration request, the response may include all of the metadata it has configured for the client - which may differ from the values sent in the request - including the following:

  • client_id: the unique client_id
  • client_secret and client_secret_expires_at: Used by confidential clients to authenticate to the token endpoint.
  • registration_client_uri and registration_access_token: Optional endpoint and token which can be used in the future to update the client registration.
  • client_id_issued_at: Optional unix timestamp when the client identifier was issued.

Authentication Context Class Reference

There are three primary "Levels of Assurance" that must be balanced. Note that these are generic and the specific requirements will be specific to each application and/or provider:

Identity Assurance Level (IAL) indicates the level of confidence that the person (identity) is who they claim to be:

  • None (Level 0): Anonymous (or equivalent) - the user could be anyone
  • Low (Level 1): Self-asserted identity
  • Medium (Level 2): Automated identity verification (eg: document verification, address verification, biometric collection, etc...)
  • High (Level 3): Manual identity proofing (address verification and biometrics mandatory)

Authentication Assurance Level (AAL) indicates the level of confidence that the request is legitimate and uncompromised:

  • None (Level 0): Unverified (or equivalent) - no authentication or a known compromised authentication method
  • Low (Level 1): Single-factor authentication - eg: password/PIN
  • Medium (Level 2): Two-factor authentication
  • High (Level 3): Multiple categories of authentication (eg: embedded tamper-resistant hardware cryptographic key + PIN + biometrics)

Federation Assurance Level (FAL) indicates the level of confidence that all communication between the identity provider and the use of confidential information in secure:

  • None (Level 0): Unprotected (or equivalent) - no meaningful verification of authentication server or public/compromised verification
  • Low (Level 1): Simple protection - eg: bearer token signed using approved cryptography
  • Medium (Level 2): Multi-layer protection - eg:  the bearer assertion is encrypted using approved cryptography
  • High (Level 3): Server-blind protection - eg: Single-use cryptographic key used to encrypt communication and provided only to the end user for use in decryping the communication

Duende IdentityServer

A functional and dependable third-party library that was designed specifically to implement OAuth2 and OpenID Connect in .Net. It is incredibly thorough - requiring very little code to add common functionality such as creating, authorizing, and authenticating users in your own database, integrating with third-party providers (Google, Microsoft, Facebook, etc...) including custom providers. Their website includes a lot of documentation on how their software works, how to get running quickly with it, and how to customize it.

I found Duende from the Microsoft Identity documentation, where Microsoft recommended them to developers who wanted to implement the type of functionality that I was starting to work on.

Duende does require a paid license to deploy to production, but they will waive the fee for the license for some small businesses and startups. At first the fee seemed a little expensive to me, but after working my way through their documentation and beginning work on it I can quickly see that it would cost significantly more to attempt this level of functionality on my own and is worth the investment.

Microsoft Identity

An optional identity provider that utilizes Azure and Microsoft resources to manage identities - primarily through Azure AD, Azure AD B2C, and Microsoft accounts. This is not necessary for all authentication and identity solutions but may be helpful for some applications. Link to Microsoft Identity Overview. There are a suite of libraries available for numerous languages - including frontend javascript - to consume the microsoft identity solution.

Note that the Microsoft Identity platform is in essence a AaaS solution (authentication-as-a-service) which relies on Azure AD and comes with its own pricing model which starts with a free tier but jumps straight from free to $6/user at a predefined threshold. I'm not saying that you should not consider Microsoft Identity - just "Buyer Beware".

ASP.NET Core Identity

Not to be confused with Microsoft Identity, the Microsoft.AspNetCore.Identity library adds full MVC support login functionality to ASP.Net Core applications. This includes adding functionality for registering new accounts, logging in, logging out, managing users, etc... This library is necessary because OAuth and OpenID Connect define mechanisms for authenticating with providers (including your own identity code) but does NOT define a mechanism for creating an account or logging in.

IdentityModel.AspNetCore

The Duende IdentityServer documentation explicitly recommends using the IdentityModel library - which they maintain - for access token lifetime management (refresh/logout functionality). Since this is a free library under the Apache License 2.0 license and it is maintained by the same team that maintains the Duende IdentityServer libraries it seems pretty safe and reasonable to follow their recommendation if you are also using Duende IdentityServer.

Entity Framework

A code-first data definition and access solution that abstracts the database from the application code. Use of entity framework allows things like unit testing data access and manipulation, automatic database deployment and migration on application startup, switching backend databases, and more. This is not specific to authentication and identity solutions but is used by Duende IdentityServer, IdentityModel.AspNetCore, and many other libraries and is worth taking some time getting used to if you are not already familiar with it.