Getting started with OpenID Connect
Steve Moss
Introduction
This post is a snapshot of my experience trying to implement OpenID Connect flows for authentication and authorization using the Katana Project v3 RC2 middleware and IdentityServer3 beta 1.
The first thing that is important to realise is that OpenID Connect has its own specification (which you should read) and should be treated as distinct from its precursors such as OAuth 1.0, OAuth 2.0 and OpenID.
My goal was to set up an OpenID Connect authorization code flow using available pluggable components. This has meant trying out pre-release code and my conclusion (in August 2014) is that this isn't something you can do yet, without non-trivial custom coding.
Terminology Muddle
Conceptually we are trying to achieve two things:
- Authenticate the user
- Authorize the authenticated user to do something
But you will run into overloaded or confusing terminology everywhere. For example:
- The OpenID Connect specification initially starts by defining the thing that does authentication and authorization as the "OpenID Provider" (which I think could anyway be better named the "OpenID Connect Provider" to avoid confusion with previous OpenID specifications, which also define an OpenID Provider).
- When going into the detail of the flows, the specification then starts using the term "Authorization Server" instead of "OpenID Provider" and then makes statements like "Authorization Server Authenticates the End-User", which to me is not helpful in making it clear what role each component plays.
So for the purposes of this article I will use the term OpenID Connect Provider abbreviated to OCP to refer to the component(s) responsible for authentication and authorization in OpenID Connect flows.
Available Components
My goal was to implement the OpenID Connect Authorization Code Flow using pluggable components, without much custom coding, to give browser-based access to a web-application.
The components I chose to investigate are introduced below. The main thing to note is that although Google, for example, has "aligned" its login process with OpenID Connect, at the current time it is still new and everything I am looking at is pre-release.
Microsoft's OpenID Connect OWIN Middleware
I added the Microsoft.Owin.Security.OpenIdConnect 3.0.0-rc2 (pre-release) NuGet package (see the Katana Project for details).
Then I was able to wire up the OpenIdConnectAuthenticationMiddleware using the supplied extension. The outline pattern being:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
[....]
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
[....]
});
So what does this middleware do? Well, in order to make it easy to plumb in, it seems to have the ambition of doing "everything" relating to OpenID Connect flows. In other words, detecting when a user needs to be redirected to the OCP, and then handling the various interactions with the OCP required.
I say ambition because this initial (pre) release has very limited functionality and flow-support, as I will describe below.
Thinktecture OpenID Connect Provider
Dominick Baier is a bit of a guru on authentication and authorization and has has an OpenID Connect Provider which he calls IdentityServer3 (not to be confused with his Identity Server v2 for OAuth2). As of 1st August 2014 this is at Beta 1 release.
The purpose of IdentityServer3 is to provide authentication and authorization, which the OWIN middleware can interact with, following pre-defined flows.
What works out of the box?
A fairly early realisation is that my goal of implementing an authorization code flow was not possible out of the box.
In broad terms the flows supported at this point in time are as follows:
Protocol | OpenIdConnectAuthenticationMiddleware | IdentityServer3 |
---|---|---|
Implicit Flow | Yes | Yes |
Code Flow | No | Yes |
Hybrid Flow | Yes | No |
This does mask a lot of detail, which is set out in the sections below, based on what I found by trial and error.
Flows Supported by OpenIdConnectAuthenticationMiddleware
Implicit Flow is easy for the middleware to support, as it just has to redirect the user to the OCP, then its role is complete. As described below, this works fine.
It took a while debugging source code to discover that the OpenIdConnectAuthenticationMiddleware only supports hybrid-flow in the first version, and not (the simpler) code-flow.
The clue was found in OpenIDConnectAuthenticationHandler.AuthenticateCoreAsync
method:
// code is only accepted with id_token, in this version, [...]
// OpenIdConnect protocol allows a Code to be received without the id_token
if (string.IsNullOrWhiteSpace(openIdConnectMessage.IdToken))
{
logger.WriteWarning("The id_token is missing.");
return null;
}
Therefore the response from the OCP must include the access-code and the ID token, which is (according to the OpenID Specification), hybrid-flow.
Hybrid Flow And Hash Fragments
The specification says Hybrid Flow should return the access code and token(s) in the hash-fragment of the URL, which is meant for a JavaScript client to read.
So how does the (server-side) middleware receive the access-code and token(s) in this case?
The answer seems to be that the JavaScript agent should parse the hash-fragment and take anything it needs (the access-token, perhaps) and then re-package the access-code and token(s) and HTTP-post them to the Client, which the OWIN middleware can then extract and process.
The OpenID Connect specification actually gives a "non-normative" example of doing this in:
- 15.5.3 Redirect URI Fragment Handling Implementation Notes
The OpenIdConnectAuthenticationMiddleware seems to expect this to be the process used, as the only Response Mode it supports is:
- "form-post"
The OpenID Connect specification also allows for "query" & "fragment" but these are not supported in this version of the middleware (see http://katanaproject.codeplex.com/workitem/313)
Flows Supported by IdentityServer3
From AuthorizeRequestValidator.ValidateProtocol we can see that two flows are supported:
- Code Flow
- Response Types
- "code"
- Response Types
- Implicit Flow
- Response Types
- "token" (legacy from OAuth2, don't use with OpenID Connect)
- "id_token"
- "id_token token"
- Response Types
//////////////////////////////////////////////////////////
// match response_type to flow
//////////////////////////////////////////////////////////
if (_validatedRequest.ResponseType == Constants.ResponseTypes.Code)
{
Logger.Info("Flow: code");
_validatedRequest.Flow = Flows.Code;
_validatedRequest.ResponseMode = Constants.ResponseModes.Query;
}
else if (_validatedRequest.ResponseType == Constants.ResponseTypes.Token ||
_validatedRequest.ResponseType == Constants.ResponseTypes.IdToken ||
_validatedRequest.ResponseType == Constants.ResponseTypes.IdTokenToken)
{
Logger.Info("Flow: implicit");
_validatedRequest.Flow = Flows.Implicit;
_validatedRequest.ResponseMode = Constants.ResponseModes.Fragment;
}
IdentityServer3 does not yet support Hybrid Flow.
Implicit Flow Configuration
Implicit Flow has a number of key defining features:
- It is intended to be used by something like a JavaScript Single Page Application (SPA) which needs to communicate with the OCP directly.
- The SPA will retrieve and store the token(s) and subsequently use them when communicating with (say) an API.
- The ID token (& access token if requested) is delivered in the hash-fragment of the URI, which means the tokens can only be read in the browser. The format would be something like:
https://localhost/Client/#id_token=eyJ0eXAi[...]&access_token=eyJ0eX[...]
- Refresh (offline) tokens are not supported.
OpenIdConnectAuthenticationMiddleware
The OpenID Connect Specification (3.2.2.1) says that the Response Type for implicit flow can be specified as either
- id_token (just return the ID token)
- id_token token (also return the access token)
So the middleware can be configured for the second case, as follows:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType("External Bearer");
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "implicitclient",
Authority = "http://localhost/IdentityServer3/core/",
RedirectUri = "https://localhost/Client/",
// "id_token" just returns the ID token.
// "id_token token" also returns the ID token and the access token
ResponseType = "id_token token",
ResponseMode = "fragment",
Scope = "openid email",
});
The middleware can auto-configure itself to work with the OCP, provided that the OCP publishes information about itself at:
- /.well-known/openid-configuration
Identity Server 3 does just that, with the following JSON returned:
{
"issuer": "https://localhost/IdentityServer3",
"jwks_uri": "http://localhost/IdentityServer3/core/.well-known/jwks",
"authorization_endpoint": "http://localhost/IdentityServer3/core/connect/authorize",
"token_endpoint": "http://localhost/IdentityServer3/core/connect/token",
"userinfo_endpoint": "http://localhost/IdentityServer3/core/connect/userinfo",
"end_session_endpoint": "http://localhost/IdentityServer3/core/connect/endsession",
"scopes_supported": ["openid","profile","email","read","write","offline_access"],
"response_types_supported": ["code","token","id_token","id_token token"],
"response_modes_supported": ["form_post","query","fragment"],
"grant_types_supported": ["authorization_code","client_credentials","password","implicit"],
"subject_types_support": ["pairwise","public"],
"id_token_signing_alg_values_supported": "RS256"
}
Thus the first thing the Katana middleware does is to visit this endpoint and configure itself accordingly.
IdentityServer 3
IdentityServer3 is in beta stage at the time of writing, and the configuration to register / define a Client is just hard-coded into a class called Clients. The relevant entry for our implicit flow Client is:
new Client
{
ClientName = "Implicit Clients",
Enabled = true,
ClientId = "implicitclient",
Flow = Flows.Implicit,
ClientUri = "https://localhost/Client/",
[...]
RequireConsent = true,
AllowRememberConsent = true,
RedirectUris = new List<Uri>
{
// JavaScript client
new Uri("http://localhost:21575/index.html"),
// OWIN middleware client
new Uri("https://localhost/Client/")
},
ScopeRestrictions = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
"read",
"write"
},
IdentityTokenSigningKeyType = SigningKeyTypes.Default,
SubjectType = SubjectTypes.Global,
AccessTokenType = AccessTokenType.Jwt,
IdentityTokenLifetime = 360,
AccessTokenLifetime = 360,
},
Time to login!
Something needs to start the login process, so I just created a dummy controller action with an [Authorize] attribute. Trying to call the corresponding URL while not logged in means the OWIN middleware will redirect you to the start of the login process.
And it works.
IdentityServer3 provides us with a login box, followed by a consent screen:
Before finally returning the requested tokens in the hash-fragment of the redirect URI:
https://localhost/Client/#id_token=eyJ0eXAi[...]&access_token=eyJ0eX[...]
To make this flow useful we would now need to write some JavaScript to get the tokens from the hash-fragment, store them, then send them with each request to our API.
Authorization Code Flow Configuration
Code Flow has a number of key defining features:
- The user goes through the same steps of a login screen and consent screen, as for the implicit flow described above.
- But instead of sending the token(s) back at the end of this process, the OCP returns an "Authorization Code" instead.
- The (server-side) Client receives this code then logs into the OCP directly and exchanges this code for the ID token and access token.
- The Client then uses the token(s) on behalf of the user without the user-agent (eg a browser) ever handling the token(s) directly.
- The Client can also request a refresh (offline access) token, which it can use to get new access tokens from the OCP when tokens expire.
This isn't fully supported / implemented with the components I have looked at, so the following sections show how I arrived at that conclusion.
OpenIdConnectAuthenticationMiddleware
The OpenID Connect Specification (3.1.2.1) says that the Response Type for code flow must be specified as
- code
This is how I tried to configure the Katana Middleware:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = CookieAuthenticationDefaults.AuthenticationType
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = "codeclient",
ClientSecret = "secret",
Authority = "http://localhost/IdentityServer3/core/",
RedirectUri = "https://localhost/Client",
ResponseType = "code",
ResponseMode = "query",
Scope = "openid email",
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
});
The middleware can auto-configure itself for the OCP, as before.
The flow won't get far enough to worry about the cookie configuration, but my intent is that the middleware retrieves the tokens then stores them in session for the user. That way, the tokens never leave the server and are just related to the user via the session, with the session cookie round-tripping to the user-agent as normal.
IdentityServer3
The hard-coded Client configuration is this time:
new Client
{
ClientName = "Code Flow Clients",
Enabled = true,
ClientId = "codeclient",
ClientSecret = "secret",
Flow = Flows.Code,
RequireConsent = true,
AllowRememberConsent = true,
ClientUri = "https://localhost/Client",
[...]
RedirectUris = new List<Uri>
{
// OWIN middleware client
new Uri("https://localhost/Client")
},
ScopeRestrictions = new List<string>
{
Constants.StandardScopes.OpenId,
Constants.StandardScopes.Profile,
Constants.StandardScopes.Email,
Constants.StandardScopes.OfflineAccess,
"read",
"write"
},
IdentityTokenSigningKeyType = SigningKeyTypes.Default,
SubjectType = SubjectTypes.Global,
AccessTokenType = AccessTokenType.Reference,
IdentityTokenLifetime = 360,
AccessTokenLifetime = 360,
AuthorizationCodeLifetime = 120
},
Note the ClientId and ClientSecret which allows the Client to login to the OCP (over TLS) before requesting the token(s).
Time to login!
The first part of the login process works fine. We get a login box and consent screen, as before. And in line with the OpenID Connect specification, we get an authorization code sent back to the Client, in the query string, in the format:
https://localhost/Client?code=e92f6e[...]=OpenIdConnect.AuthenticationProperties=fkVPi[...]
Code Flow then expects the Client to contact the OCP directly, and exchange the code for the ID token and access token.
But this doesn't happen. The OpenIdConnectAuthenticationMiddleware doesn't include any code to handle this type of response.
Why doesn't it work?
As described in "What works out of the box?" above:
- The only flow supported by this version of the middleware is Hybrid Flow, with the access-code and ID token returned to the Client in a form post.
- The only flows supported by the beta version of IdentityServer3 are Code Flow, with the access-code returned in the Query String and Implicit Flow, with the token(s) returned in the Hash Fragment.
So there is a mismatch both in the flows supported and the return types supported, and clearly code-flow is not possible out of the box.
Supported Response Modes
Subsequent to my tests (above) the Katana team realised it was misleading to allow the middleware to be configured to request a response in the Query String, since this is not supported:
ResponseMode = "query",
So as of this closed Katana issue the ResponseMode is now defaulted to "form_post" and the ResponseMode property has been removed from the configuration.
Section 13 of the OpenID Connect specification says it should be possible to serialize messages using any of the following:
- Query String Serialization
- Form Serialization
- JSON Serialization
So I would expect this ResponseMode configuration to be re-instated in future versions.
Hybrid Flow Configuration
As the name suggests, Hybrid Flow has some of the characteristics of Code Flow (an access code is returned) and some of Implicit Flow (an ID token and access token can be returned).
The access code and tokens are returned in the URL's hash-fragment to the User Agent. So if the Client also requires the access-code or token(s) the User Agent has to extract & repackage the items (eg in JavaScript) and POST them on to the (server-side) Client.
OpenIdConnectAuthenticationMiddleware
The OpenID Connect Specification (3.3.2.1) says that the Response Type for hybrid flow must be specified as any of:
- code id_token
- code id_token token
- code token (legacy from OAuth2, don't use with OpenID Connect)
The variants controlling whether the ID token, access-token, or both are returned.
So the middleware would be configured to return everything, as follows:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType("External Bearer");
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = "hybridclient",
Authority = "http://localhost/IdentityServer3/core/",
RedirectUri = "https://localhost/Client/",
ResponseType = "code id_token token",
ResponseMode = "fragment", // maybe "form_post"?
Scope = "openid email",
});
There is implementation to process the ID token received and create an AuthenticationTicket in:
OpenIdConnectAuthenticationHandler.AuthenticateCoreAsync
But there is no implementation to convert the access-code into tokens (although an AuthorizationCodeReceivedNotification is published, which you can hook into with your own implementation).
IdentityServer3
At this point I looked at IdentityServer3 and found it doesn't yet support Hybrid Flow.
If you request any of the hybrid flow response types, such as "code id_token token", this is rejected as not supported by the AuthorizeRequestValidator class.
Refresh Tokens
Another reason for wanting to use Code Flow is to allow the use of Refresh Tokens.
In our case, we have various server-side processes that need to run on behalf of the user, but without the user's involvement. Therefore if the access-token has expired, the service needs to be able to request a new one, using the refresh-token.
There is a blog post for IdentityServer 3 that describes the current refresh token support.
Conclusions
Implicit Flow is the only flow which works out of the box, with the components evaluated, at this point in time.
Authorization Code Flow is what I want to use, but this isn't implemented yet in the middleware being evaluated. Fortunately the Katana Project contains other examples, such as GoogleOAuth2AuthenticationMiddleware, which does implement code-flow, so it should be possible to write some custom middleware using the existing code to kick-start things.
Comments
Thanks! great article, clearly sums up the different issues I also encountered while trying to achieve the same results.<br /><br />Could you elaborate on how did handled the refresh token issue? How do you refresh your token from the client side processes, or must you post to your back channel to obtain a new token?
RonyClients should get a new token, if the previous one has expired (probably using a backchannel, as you say, if your authentication is still valid, eg via a cookie from the auth server).<br /><br />Refresh-tokens are more accurately called "offline-access tokens". They are only meant to be used server-side when doing something long-lived on behalf of the user and the original access token has expired.<br /><br />Refresh tokens are not allowed client-side as this would be a security risk. If the token was hijacked the hijacker would be able to keep a session going indefinitely, probably without anyone noticing. This point isn't that clear in my post, above!
Steve MossGreat article. Have you found any info on whether Microsoft's middleware will eventually implement code flow or if there is another one that does?
Steve P@Steve P - I don't know if the Katana project will end up creating a general purpose code-flow middleware or not.<br /><br />I ended up creating one of my own. The starting point is the OpenID Connect specification, as a checklist of what needs to happen. The Katana project (https://katanaproject.codeplex.com/) does have middleware for Google, Microsoft etc from which you can copy
Steve MossAccording to this:<br />http://leastprivilege.com/2014/10/10/openid-connect-hybrid-flow-and-identityserver-v3/<br />Hybrid Flow support has been added to Identity Server 3 since Beta 2 in October 2014
TomaszGreat article! Thank you for sharing your experience.<br />I'm also struggling to try to implement the same thing.<br />I've juste one question, are you sure about the way you "inject" OpenID Connect to the IdentityServer3 middleware juste by changing the hard coded Client class? Because I think this class is juste a sample of an "InMemory" Clients factory witch represents a fake set of registred clients to the OCP. So I think there is another way of adding the config to the middleware maybe by using "UseIdentityServerBearerTokenAuthentication" method.<br />What you think?<br />Regards
AhmedGreat article, thanks for positing. One thing I disagree with from the comments is regarding the use of refresh tokens. Refresh tokens are certainly allowed on the client side and are designed to be more secure and more manageable. <br /><br />Before I start its really important to note/remember that the Auth server and Resource server in OAuth2 are completely separate. They are combined in the default implementation of a WebAPI project but can be hosted on completely separate servers.<br /><br />When making a request with an access token you are making a request to the resource server NOT the auth server. By default the resource server validates the access token is valid without contacting the auth server OR a database by using the expiry date, signing information etc of the token. An access token does not require the client id or secret to be passed with the call and so unless you have custom logic in your resource server to check the token validity, compromised long life security tokens can provide a real risk. Also, if you are using claims to do authorization etc then these claims will not be able to be refreshed and so updates to user roles etc will not obtained until a new ticket is generated. For these reasons it is wise to have short lived access tokens of a couple of mins or so. <br /><br />This however raises a problem. Normally, in order to generate a valid access token we need the clients credentials. If our access tokens expire every 2 mins then this would require the client to re-login every 2 mins - not very ideal really. <br /><br />This is where the refresh token comes in. The refresh token allows the client - or server - application to request a new access token based on the refresh token. Thus, once a user signs in, the client or server application can, in the background, request new access tokens based on this refresh token. Refresh tokens are normally generated with a long life, for example 1 year.<br /><br />You may now be wondering why a refresh token is any more secure than an access token and its a good question. It all comes down to the authorization flow. As mentioned above an access token is hard to revoke or change in its own right. When using refresh tokens we store the refresh token in our data repo. In our case - and this is considered an implementation detail - we allow only a single refresh token per user per client application. When the first request is made to sign in from the client, the request is validated and a new token generated, along with refresh token id. We delete any refresh tokens in the db for this client and user and then store the new refresh token id. How the client application handles the refresh token is an implementation detail of the client. Now, whenever an API operation is required, the client app can check the access token expiry, if expired can request a new one using the refresh token id without intervention from the user. Yay.<br /><br />Doing this allows wonderful flexibility and solves the issue of compromised tokens. As you prob noticed we are storing the refresh token id's in the repo / db. If a refresh token is obtained by a malicious person 1) it is invalidated as soon as the next operation is performed on the real system - a new refresh token is generated on each request for an access token thus the malicious app becomes out of sync and will be invalidated. 2) As we store the refresh token in the DB you can invalidate it using a backend tool. All you have to do is delete the token from the db and any access requests will be denied. This allows you to log a user (or all users) out of all client applications using an admin page in your backend. Wonderful stuff :) <br /><br />For another great article on how to setup refresh tokens see here: http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/
Kevin@Kevin - thanks for your detailed comments!<br /><br />I go into the question of how to use offline-access tokens in a separate post http://appetere.com/post/how-to-renew-access-tokens<br /><br />The point I would make is that while it is of course possible to use offline-access tokens as you have described, along with the ways you've described to mitigate security problems, this is not the intent of this type of token within OpenID Connect.<br /><br />I started going down the route you have described early on, but then used some consultancy from Dominick Baier (one of the authors of IdentityServer3) which led to my other post.<br /><br />On your specific point that a 2 minute access-token would mean a User would have to log-in every 2 minutes, this is not usually the case for browser-based Clients. IdentityServer3 issues a cookie of its own to the browser, which proves the User authenticated. This cookie usually has a long duration (it defaults to 10 hours, from memory). <br /><br />When the access-token expires, the Client software just repeats the original authentication/token request, but because the IdentitySever3 cookie is there, it will not require the User to enter their credentials again. There is more on this in my post.
Steve MossComments are closed