OAuth 2
OAuth 2 is supported using Owin middleware, so you have to use Owin.
Roles
- Resource owner - the end user
- Client - an application making requests on behalf of the resource owner. Maybe a browser, server or smartphone app.
- Resource server - the application/ API (it hosts protected resources)
- Authorization server - the server issuing access tokens (Facebook, Google...).
In WS-Federation terms, Authorization server is an STS/Identity Provider (IdentityServer v3 and ADFS v4 support both OAuth2 and Ws-fed). The Relying Party/Service Provider role is split into two: client (uses data) and resource server (provides data).
Tokens
Access tokens are like cookies in traditional forms authentication:
- There are passed in the HTTP header Authentication: Bearer xxxx
- The format is a base64 encoded JWT (json web token). It's not encrypted or signed- you must use SSL
- You can refresh them: grant_type=refresh_token. Which can update the information.
Flows
The most common interactions are implicit/ token (javascript in the browser) and code (server page gets code and does a server-to-server post for the token). The latter never exposes the token, so it's more secure.
Generally you have to register the client with the auth server (so it knows where to redirect, or at least validate your redirect_uri).
Implicit Flow (Browsers/Apps)
- Implicit / client-side (response_type=token): browser-based redirect to auth server, which logs in/shows consent screen.
- → https://server/auth?client_id=1&response_type=token
The query-string has a client_id and may have a redirect_uri and scope (e.g. read, update...). - ← https://client/callback#token=xxx
The consent screen redirects back with the token in the url hash: location.hash.substr(1) - Best practice is to do another ajax call to validate the token (which returns json with the audience, which must match your client)
- Then send ajax calls for data with http header "Authorization: Bearer xxx"
- → https://server/auth?client_id=1&response_type=token
Authorization Code Flow (Server-side)
- Authorization Code (response_type=code): for server-side authentication.
- → https://server/auth?response_type=code&client_id=x
User redirected to auth server login page. When they log in/consent... - ← https://client/?code=xxx
The auth server redirects with a ?code=xxx to the client. - → POST https://server/grant_type=authorization_code&code=xxx&
client_id=CLIENT_ID&client_secret=CLIENT_SECRET
The client POSTs to a token endpoint with grant_type=authorization_code
(this is a "back channel" call from the server, the user can never see your client secret or the access token) - ← { "access_token":"hm4s8..." }
The auth server replies with the access_token in json.
- → https://server/auth?response_type=code&client_id=x
Password (Secure servers only)
- Password (also "response owner password credential flow") (grant_type=password): send username and password to authorization server (via Basic Auth or form-encoded), get token. This is a replacement to HTTP Basic Authentication.
Client Credential (application-level access)
- → POST https://server/?grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
Intended for app access (get list of users etc)
OpenID Connect
OpenID Connect is an authentication protocol built on top of OAuth2. OpenID identity providers (IdP) include 3rd parties like Google and Microsoft, plus your own with IdentityServer or ADFS 4.
- install-package Microsoft.Owin.Security.Cookies
- install-package Microsoft.Owin.Security.OpenIdConnect
In Startup do the following:
- Add UseCookieAuthentication (first!)
- Add UseOpenIdConnectAuthentication
- Add the IdP url ("Authority") and the Client's ID and RedirectUri
- In Scope, add the claims you'll need (otherwise you just get openid claims).
- In Notifications, the SecurityTokenValidated event allows for claims transformation (standardise the claim names e.g. Ws-fed like Uris or short names).
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44324/core", //IdP Url,
ClientId = "tst-infrabel.be", //OpenID server must have this client
Scope = "openid profile roles", //what roles I want (default is openid)
RedirectUri = "https://localhost:44348/", //start page for this application
ResponseType = "code id_token token",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
//logging errors
Console.WriteLine(context.Exception);
return Task.FromResult(0);
},
//claims transformation
SecurityTokenValidated = notification =>
{
var id = notification.AuthenticationTicket.Identity;
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(id.AuthenticationType);
//copy the claims in
nid.AddClaim(id.FindFirst(ClaimTypes.Name));
nid.AddClaims(id.FindAll(ClaimTypes.Role));
// add new claim
nid.AddClaim(new Claim("LogonTime", DateTime.UtcNow.ToString("u")));
notification.AuthenticationTicket =
new AuthenticationTicket(nid, notification.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
The client must use SSL. If you don't (e.g. local development), the client will enter an infinite redirect loop back to the IdP (you can also have this if the client URL doesn't end in "/").
In app.UseCookieAuthentication(new CookieAuthenticationOptions
add:
CookieSecure = CookieSecureOption.Never
Obviously don't leave this is production- use the [RequireHttps]
attribute.
SignOut
Use Request.GetOwinContext().Authentication.SignOut();
Client
A simple console client
static void Main(string[] args)
{
var token = GetToken().Result;
Console.WriteLine(token);
var data = CallService(token).Result;
Console.WriteLine(data);
}
private async static Task<string> GetToken()
{
using (var client = new HttpClient())
{
var post =
new Dictionary<string, string>
{
{"grant_type", "password"},
{"username", "alice"},
{"password", "secret"},
//client
{"client_id", "1"},
{"client_secret", "secret"},
};
var response = await client.PostAsync("http://localhost:4746/token",
new FormUrlEncodedContent(post));
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(content);
return json["access_token"].ToString();
}
throw new InvalidOperationException(response.ReasonPhrase);
}
}
private async static Task<string> CallService(string token)
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
return await client.GetStringAsync("http://localhost:4746/api/data");
}
}