One of the features we added in Beta 2 is support for hybrid flow (see spec). What is hybrid flow – and why do I care?
Well – in a nutshell – OpenID Connect originally extended the two basic OAuth2 flows (or grants) called authorization code and implicit. Implicit allows requesting tokens without explicit client authentication (hence the name), but uses the redirect URI instead to verify client identity. Because of that, requesting long lived tokens like a refresh token is not allowed in that flow.
Authorization code flow on the other hand only returns a so called authorization code via the unauthenticated front channel, and requires client authentication using client id and secret (or some other mechanism) to retrieve the actual tokens (including a refresh token) via the back channel. This mechanism was originally designed for server-based applications only, since storing client secrets on a client device is questionable without the right security mechanisms in place.
Hybrid flow (as the name indicates) is a combination of the above two. It allows to request a combination of identity token, access token and code via the front channel using either a fragment encoded redirect (native and JS based clients) or a form post (server-based web applications). This enables e.g. scenarios where your client app can make immediate use of an identity token to get access to the user’s identity but also retrieve an authorization code that that can be used (e.g. by a back end service) to request a refresh token and thus gaining long lived access to resources.
Lastly, hybrid flow is the only flow supported by the Microsoft OpenID Connect authentication middleware (in combination with a form post response mode), and before we added support for hybrid flow to IdentityServer, interop was a bit complicated (see here).
Our samples repo has two clients using hybrid flow – native and web. Let’s have a look at the middleware based one.
You are using hybrid flow whenever you have a response type of code combined with some other response type, e.g. id_token or token (or both).
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = “katanaclient”,
Authority = Constants.BaseAddress,
RedirectUri = Constants.RedirectUri,
PostLogoutRedirectUri = Constants.PostLogoutUri,
ResponseType = “code id_token token”,
Scope = “openid email profile read write offline_access”,
SignInAsAuthenticationType = “Cookies”
};
The scopes are a combination of identity scopes allowing us to retrieve user identity (email and profile), resource scopes for api access (read and write) and a request for a refresh token (offline_access).
After authentication (and consent), the middleware will validate the response and notify you when it has retrieved the authorization code from the callback form post. You then typically have a number of responsibilities, e.g.
- Contact the userinfo endpoint to retrieve the claims about the user
- Transform the claims to whatever format your application expects
- Store the access token for later use (along with information about its lifetime)
- Store the refresh token so you can refresh expired access tokens (if long lived access is needed)
- Store the id_token if you need features at the OpenID Connect provider that requires id token hints (e.g. redirects after logging out)
In our sample, I first strip all the protocol related claims from the identity token:
// filter "protocol" claims
var claims = new List<Claim>(from c in n.AuthenticationTicket.Identity.Claims
where c.Type != "iss" &&
c.Type != "aud" &&
c.Type != "nbf" &&
c.Type != "exp" &&
c.Type != "iat" &&
c.Type != "nonce" &&
c.Type != "c_hash" &&
c.Type != "at_hash"
select c);
…then get the user claims from the user info endpoint
// get userinfo data
var userInfoClient = new UserInfoClient(
new Uri(Constants.UserInfoEndpoint),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
userInfo.Claims.ToList().ForEach(ui => claims.Add(new Claim(ui.Item1, ui.Item2)));
…and retrieve a refresh token (and a fresh access token)
// get access and refresh token
var tokenClient = new OAuth2Client(
new Uri(Constants.TokenEndpoint),
"katanaclient",
"secret");
var response = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
Then I combine all the claims together and store them in the authentication cookie:
claims.Add(new Claim("access_token", response.AccessToken));
claims.Add(new Claim("expires_at",
DateTime.Now.AddSeconds(response.ExpiresIn).ToLocalTime().ToString()));
claims.Add(new Claim("refresh_token", response.RefreshToken));
claims.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(claims.Distinct(new ClaimComparer()),
n.AuthenticationTicket.Identity.AuthenticationType),
n.AuthenticationTicket.Properties);
You might not need all of the above steps, but this is how it generally works. HTH.
Filed under: IdentityServer, Katana, OAuth, OpenID Connect, OWIN, WebAPI
