I'm using the Properties column in my OpenIddict applications to store some metadata about an application instead of using a custom entity - comments on this post custom properties within token imply that's what its meant for.
However, despite reading the above post I'm struggling to understand how best to implement this.
Currently my "create application" looks like this:
public interface IOpenIddictAppService
{
Task<CreateOpenIddictAppResponseDto> CreateAsync(CreateOpenIddictAppRequestDto appDto);
}
public class OpenIddictAppService : IOpenIddictAppService
{
private readonly IOpenIddictApplicationManager _appManager;
public OpenIddictAppService(IOpenIddictApplicationManager appManager)
{
_appManager = appManager;
}
public async Task<CreateOpenIddictAppResponseDto> CreateAsync(CreateOpenIddictAppRequestDto appDto)
{
var sha = SHA512.Create();
var clientId = Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())));
var clientSecret = SecureRandomStringHelper.Create(StaticData.ClientSecretSize);
var data = new OpenIddictApplicationDescriptor
{
ClientId = clientId,
ClientSecret = clientSecret,
DisplayName = appDto.Name,
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Prefixes.Scope + "api",
OpenIddictConstants.Permissions.ResponseTypes.Code
}
};
// Postman test URL"https://oauth.pstmn.io/v1/callback"
appDto.RedirectUrls.Split(" ").ToList().ForEach(x => data.RedirectUris.Add(new Uri(x)));
data.Properties.Add("IdentityConfig", JsonSerializer.SerializeToElement(new AppIdentityProperties
{
ClientSystemId = appDto.ClientSystemId,
CustomerAccountId = appDto.CustomerAccountId
}));
var app = await _appManager.CreateAsync(data);
CreateOpenIddictAppResponseDto result = new()
{
Id = new Guid(await _appManager.GetIdAsync(app) ?? throw new ArgumentNullException("OpenIddictApplication not found")),
ClientId = clientId, // Send back the client id used
ClientSecret = clientSecret, // Send back the secret used so it can be displayed one-time-only for copy/paste
RedirectUrls = string.Join(" ", await _appManager.GetRedirectUrisAsync(app))
};
return result;
}
}
And I'm loading it like this in my authorization controller:
[HttpPost("~/connect/token")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
ClaimsPrincipal claimsPrincipal;
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application details cannot be found in the database.");
}
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the tokens (use the client_id as the subject identifier).
identity.AddClaim(Claims.Subject, await _applicationManager.GetClientIdAsync(application))
.AddClaim(Claims.Name, await _applicationManager.GetDisplayNameAsync(application));
var properties = await _applicationManager.GetPropertiesAsync(application);
if (properties.Any(o => o.Key == "IdentityConfig"))
{
var identityConfig = JsonSerializer.Deserialize<AppIdentityProperties>(properties.FirstOrDefault(o => o.Key == "IdentityConfig").Value);
if (identityConfig != null)
{
identity.AddClaim(StaticData.Claims.ClientSystem, identityConfig.ClientSystemId.ToString())
.AddClaim(StaticData.Claims.CustomerAccount, identityConfig.CustomerAccountId.ToString());
}
}
identity.SetDestinations(static claim => claim.Type switch
{
// Allow the "name" claim to be stored in both the access and identity tokens
// when the "profile" scope was granted (by calling principal.SetScopes(...)).
Claims.Name when claim.Subject.HasScope(Scopes.Profile)
=> new[] { Destinations.AccessToken, Destinations.IdentityToken },
// Otherwise, only store the claim in the access tokens.
_ => new[] { Destinations.AccessToken }
});
// Note: In the original OAuth 2.0 specification, the client credentials grant
// doesn't return an identity token, which is an OpenID Connect concept.
//
// As a non-standardized extension, OpenIddict allows returning an id_token
// to convey information about the client application when the "openid" scope
// is granted (i.e specified when calling principal.SetScopes()). When the "openid"
// scope is not explicitly set, no identity token is returned to the client application.
// Set the list of scopes granted to the client application in access_token.
claimsPrincipal = new ClaimsPrincipal(identity);
claimsPrincipal.SetScopes(request.GetScopes());
claimsPrincipal.SetResources(await _scopeManager.ListResourcesAsync(claimsPrincipal.GetScopes()).ToListAsync());
}
else if (request.IsAuthorizationCodeGrantType())
{
// Retrieve the claims principal stored in the authorization code
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
}
else if (request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the refresh token.
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
}
else
{
throw new InvalidOperationException("The specified grant type is not supported.");
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
Now - it's working...but I have a sneaky feeling that I'm just using it wrong especially based on Kevin Chalet's comments on that post... I think I'm probably also setting the redirect URLs the wrong way too!
Can anyone give me any more specific guidance on how I should really be doing this.
Related
I have been testing some code to sign in users to their Microsoft/school/work accounts using raw HttpRequestMessage and HttpResponseMessage. I know there are libraries available to do this but I want to test the raw approach as well (especially usage of refresh tokens), while looking for the right library to handle it.
I'm currently learning authentication, with limited knowledge of ASP.NET/Core.
I'm following this guide: https://learn.microsoft.com/en-us/graph/auth-v2-user
I've just modified the SignIn() method in AccountController in an example project that used more high level libraries to sign in.
I'm requesting an authorization code.
The SignIn() code:
public void SignIn()
{
using (var httpClient = new HttpClient())
{
try
{
var tenant = "my tenant id";
var clientId = ConfigurationManager.AppSettings["ida:AppID"];
var responseType = "id_token+code";
var redirectURI = ConfigurationManager.AppSettings["ida:RedirectUri"];
var responseMode = "form_post";//query";
var appScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
var scopes = $"openid profile offline_access {appScopes}";
var state = "12345";
//var prompt = "consent";
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize", tenant);
var body = string.Format("client_id={1}&response_type={2}&redirect_uri={3}&response_mode={4}&scope={5}&state={6}", tenant, clientId, responseType, redirectURI, responseMode, scopes, state);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).Result;
var content = response.Content.ReadAsStringAsync().Result;
}
catch (Exception ex)
{
}
}
//if (!Request.IsAuthenticated)
//{
// // Signal OWIN to send an authorization request to Azure
// Request.GetOwinContext().Authentication.Challenge(
// new AuthenticationProperties { RedirectUri = "/" },
// OpenIdConnectAuthenticationDefaults.AuthenticationType);
//}
}
I'm just returning void from the method now because I'm not sure what I should return yet.
Debugging and looking at the response variable, the status code is 200, and has some other information to it. However, the content of the HttpResponseMessage, when I paste it into a file and opening it in a browser, displays (or redirects to) https://login.microsoftonline.com/cookiesdisabled, which shows a message saying that I could not be logged in because my browser blocks cookies. However, I don't think this really is the case.
How can I resolve this and have the user log in and consent, and get the authorization code?
I couldn't really find any example in ASP.NET that uses this raw approach. Is it not recommended?
You should fistly understand how OAuth 2.0 authorization code flow works in Azure AD V2.0 :
Microsoft identity platform and OAuth 2.0 authorization code flow
The general process would be like :
When login in client application, user will be redirect to Azure AD login endpoint(https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize) and provides info like which client(client_id) in which tenant(tenant id) user wants to login , and redirect back to which url(redirect_uri) after successful login.
User enter credential , Azure AD validate credential and issue code and redirect user back to redirect url provided in step 1 (Also match one of the redirect_uris you registered in the portal).
The client application will get the code and send http post request with code to acquire access token .
So if you want to manally implement the code flow in your application , you can refer to below code sample :
public async Task<IActionResult> Login()
{
string authorizationUrl = string.Format(
"https://login.microsoftonline.com/{0}/oauth2/v2.0/authorize?response_type=code&client_id={1}&redirect_uri={2}&scope={3}",
"tenantID", "ClientID", "https://localhost:44360/Home/CatchCode",
"openid offline_access https://graph.microsoft.com/user.read");
return Redirect(authorizationUrl);
}
private static readonly HttpClient client = new HttpClient();
public async Task<ActionResult> CatchCode(string code)
{
var values = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "client_id", "XXXXXX"},
{ "code", code},
{ "redirect_uri", "https://localhost:44360/Home/CatchCode"},
{ "scope", "https://graph.microsoft.com/user.read"},
{ "client_secret", "XXXXXXXXXXX"},
};
var content = new FormUrlEncodedContent(values);
//POST the object to the specified URI
var response = await client.PostAsync("https://login.microsoftonline.com/cb1c3f2e-a2dd-4fde-bf8f-f75ab18b21ac/oauth2/v2.0/token", content);
//Read back the answer from server
var responseString = await response.Content.ReadAsStringAsync();
//you can deserialize an Object use Json.NET to get tokens
}
That just is simple code sample which will get Microsoft Graph's access token , you still need to care about url encode and catch exception , but it shows how code flow works .
I have started playing with OpenID Connect server with ASOS by implementing the resource owner password credential grant. however when I test it using postman, I am getting generic 500 internal server error.
Here is my code for your debugging pleasure. I appreciate your feedback.
Thanks
-Biruk
here is my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddAuthentication(options => {
options.SignInScheme = "ServerCookie";
});
services.AddApplicationInsightsTelemetry(Configuration);
services.AddMvc();
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, LoggerFactory loggerFactory)
{
app.UseOAuthValidation();
app.UseOpenIdConnectServer(options => {
// Create your own authorization provider by subclassing
// the OpenIdConnectServerProvider base class.
options.Provider = new AuthorizationProvider();
// Enable the authorization and token endpoints.
// options.AuthorizationEndpointPath = "/connect/authorize";
options.TokenEndpointPath = "/connect/token";
// During development, you can set AllowInsecureHttp
// to true to disable the HTTPS requirement.
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
// Note: uncomment this line to issue JWT tokens.
// options.AccessTokenHandler = new JwtSecurityTokenHandler();
});
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
app.UseMvc();
}
and here is my AuthorizationProvider.cs
public sealed class AuthorizationProvider : OpenIdConnectServerProvider
{
public Task<User> GetUser()
{
return Task.Run(()=> new User { UserName = "biruk60", Password = "adminUser123" });
}
// Implement OnValidateAuthorizationRequest to support interactive flows (code/implicit/hybrid).
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
// Reject the token request that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only resource owner password credentials and refresh token " +
"are accepted by this authorization server");
return Task.FromResult(0);
}
// Since there's only one application and since it's a public client
// (i.e a client that cannot keep its credentials private), call Skip()
// to inform the server the request should be accepted without
// enforcing client authentication.
context.Skip();
return Task.FromResult(0);
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
//// Resolve ASP.NET Core Identity's user manager from the DI container.
//var manager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
// Only handle grant_type=password requests and let ASOS
// process grant_type=refresh_token requests automatically.
if (context.Request.IsPasswordGrantType())
{
// var user = await manager.FindByNameAsync(context.Request.Username);
var user = await GetUser();//new { userName = "briuk60#gmail.com", password = "adminUser123" };
if (user == null)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid credentials.");
return;
}
if (user != null && (user.Password == context.Request.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
// Note: the name identifier is always included in both identity and
// access tokens, even if an explicit destination is not specified.
// identity.AddClaim(ClaimTypes.NameIdentifier, await manager.GetUserId(user));
// When adding custom claims, you MUST specify one or more destinations.
// Read "part 7" for more information about custom claims and scopes.
identity.AddClaim("username", "biruk60",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile);
// Set the resource servers the access token should be issued for.
// ticket.SetResources("resource_server");
context.Validate(ticket);
}
}
}
}
What am i doing wrong. I can put it in debug mode and step through it without any error it just 500 internal Server Error in fiddler and postman.
Here's the exception you're likely seeing:
System.InvalidOperationException: A unique identifier cannot be found to generate a 'sub' claim: make sure to add a 'ClaimTypes.NameIdentifier' claim.
Add a ClaimTypes.NameIdentifier claim and it should work.
Im using IdentityServer3 to secure a Web API with the client credentials grant. For documentation Im using Swashbuckle but can't figure out how to enable Oauth2 in the SwaggerConfig for the client credentials (application) flow. Any help would be appreciated!
I was able to get this working. Most of the answer can be found here.
There were a few parts I had to change to get the client_credential grant to work.
The first part is in the EnableSwagger and EnableSwaggerUi calls:
config.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "sample api");
c.OAuth2("oauth2")
.Description("client credentials grant flow")
.Flow("application")
.Scopes(scopes => scopes.Add("sampleapi", "try out the sample api"))
.TokenUrl("http://authuri/token");
c.OperationFilter<AssignOAuth2SecurityRequirements>();
}).EnableSwaggerUi(c =>
{
c.EnableOAuth2Support("sampleapi", "samplerealm", "Swagger UI");
});
The important change here is .Flow("application") I also used the .TokenUrl call instead of .AuthorizationUrl This is just dependent on your particular authorization scheme is set up.
I also used a slightly different AssignOAuth2SecurityRequirements class
public class AssignOAuth2SecurityRequirements : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var authorized = apiDescription.ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>();
if (!authorized.Any()) return;
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{"oauth2", Enumerable.Empty<string>()}
};
operation.security.Add(oAuthRequirements);
}
}
This should be sufficient to get the authentication switch to show. The other problem for me was that the default authentication dialog is set up so a user just has to select a scope and then click authorize. In my case this didn't work due to the way I have authentication set up. I had to re-write the dialog in the swagger-oauth.js script and inject it into the SwaggerUI.
I had a bit more trouble getting this all working, but after a lot of perseverance I found a solution that works without having to inject any JavaScript into the SwaggerUI. NOTE: Part of my difficulties might have been due to using IdentityServer3, which is a great product, just didn't know about a configuration issue.
Most of my changes are similar to bills answer above, but my Operation Filter is different. In my controller all the methods have an Authorize tag with no Roles like so:
[Authorize]
// Not this
[Authorize(Roles = "Read")] // This doesn't work for me.
With no Roles defined on the Authorize tag the OperationFilter looks like this:
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
// Correspond each "Authorize" role to an oauth2 scope, since I don't have any "Roles" defined, this didn't work
// and is in most of the Apply methods I found online. If you are like me and your [Authorize] tag doesn't contain
// any roles this will not work.
//var scopes = apiDescription.ActionDescriptor.GetFilterPipeline()
// .Select(filterInfo => filterInfo.Instance)
// .OfType<AuthorizeAttribute>()
// .SelectMany(attr => attr.Roles.Split(','))
// .Distinct();
var scopes = new List<string>() { "Read" }; // For me I just had one scope that is added to all all my methods, you might have to be more selective on how scopes are added.
if (scopes.Any())
{
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", scopes }
};
operation.security.Add(oAuthRequirements);
}
}
The SwaggerConfig looks like this:
public static void Register()
{
var thisAssembly = typeof(SwaggerConfig).Assembly;
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion("v1", "waPortal");
c.OAuth2("oauth2")
.Description("OAuth2 Client Credentials Grant Flow")
.Flow("application")
.TokenUrl("http://security.RogueOne.com/core/connect/token")
.Scopes(scopes =>
{
scopes.Add("Read", "Read access to protected resources");
});
c.IncludeXmlComments(GetXmlCommentsPath());
c.UseFullTypeNameInSchemaIds();
c.DescribeAllEnumsAsStrings();
c.OperationFilter<AssignOAuth2SecurityRequirements>();
})
.EnableSwaggerUi(c =>
{
c.EnableOAuth2Support(
clientId: "swaggerUI",
clientSecret: "BigSecretWooH00",
realm: "swagger-realm",
appName: "Swagger UI"
);
});
}
The last part was the hardest to figure out, which I finally did with the help of the Chrome Developer tools that showed a little red X on the network tag showing the following error message:
XMLHttpRequest cannot load http://security.RogueOne.com/core/connect/token. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:62561' is therefore not allowed access.
I described this error here Swagger UI not parsing reponse which was due to IdentityServer3 correctly not adding a response header of "Access-Control-Allow-Origin:http://localhost:62561" You can force IdentityServer3 to send that header by updating you client creation to be the following:
new Client
{
ClientName = "SwaggerUI",
Enabled = true,
ClientId = "swaggerUI",
ClientSecrets = new List<Secret>
{
new Secret("PasswordGoesHere".Sha256())
},
Flow = Flows.ClientCredentials,
AllowClientCredentialsOnly = true,
AllowedScopes = new List<string>
{
"Read"
},
Claims = new List<Claim>
{
new Claim("client_type", "headless"),
new Claim("client_owner", "Portal"),
new Claim("app_detail", "allow")
},
PrefixClientClaims = false
// Add the AllowedCorOrigins to get the Access-Control-Allow-Origin header to be inserted for the following domains
,AllowedCorsOrigins = new List<string>
{
"http://localhost:62561/"
,"http://portaldev.RogueOne.com"
,"https://portaldev.RogueOne.com"
}
}
The AllowedCorsOrigins was the last piece of my puzzle. Hopefully this helps someone else who is facing the same issue
Is there a possibility to configure OAuth2 AssertionFlow with Facebook in Thinktecture Identity Server v3?
There was a post on leastprivilege.com about implementing AssertionFlow for Microsoft OAuth and AuthorizationServer but I need to integrate with Facebook and, furthermore, AuthorizationServer is marked as deprecated and it's not maintained anymore.
In response to #NathanAldenSr's comment, I publish some code of my working solution.
Server side - custom validator:
public class FacebookCustomGrantValidator: ICustomGrantValidator
{
private readonly IUserService userService;
private const string _FACEBOOK_PROVIDER_NAME = "facebook";
// ...
async Task<CustomGrantValidationResult> ICustomGrantValidator.ValidateAsync(ValidatedTokenRequest request)
{
// check assetion type (you can have more than one in your app)
if (request.GrantType != "assertion_fb")
return await Task.FromResult<CustomGrantValidationResult>(null);
// I assume that fb access token has been sent as a response form value (with 'assertion' key)
var fbAccessToken = request.Raw.Get("assertion");
if (string.IsNullOrWhiteSpace(assertion))
return await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult
{
ErrorMessage = "Missing assertion."
});
AuthenticateResult authebticationResult = null;
// if fb access token is invalid you won't be able to create Facebook client
var client = new Facebook.FacebookClient(fbAccessToken);
dynamic response = client.Get("me", new { fields = "email, first_name, last_name" });
// create idsrv identity for the user
authebticationResult = await userService.AuthenticateExternalAsync(new ExternalIdentity()
{
Provider = _FACEBOOK_PROVIDER_NAME,
ProviderId = response.id,
Claims = new List<Claim>
{
new Claim("Email", response.email),
new Claim("FirstName", response.first_name),
new Claim("LastName", response.last_name)
// ... and so on...
}
},
new SignInMessage());
return new CustomGrantValidationResult
{
Principal = authebticationResult.User
};
}
}
You can easily test it with OAuth2Client that is also provided by Thinktecture (in Thinktexture.IdentityModel Client Library nuget package).
string fbAccessToken = "facebook_access_token_you_aquired_while_logging_in";
string assertionType = "assertion_fb";
var client = new OAuth2Client(
new Uri("your_auth_server_url"),
"idsrv_client_id",
"idsrv_client_secret");
string idsrvAccessToken = client.RequestAssertionAsync(assetionType, fbAccessToken,).Result;
IdentityServer v3 also supports assertion flow. The samples wiki has two samples on that (called "Custom Grants):
https://github.com/thinktecture/Thinktecture.IdentityServer.v3.Samples/tree/master/source
Based on this tutorial http://www.asp.net/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server, I have created an Authorization Server, a Resource Server and a MVC Client.
The MVC Client has a Controller which gets some data from the Resource Server. The Resource Server requires authentication. The MVC Clients gets an authorization code from the Authorization Server and Redirects the user to the Authorization Server for authentication. Finally the MVC Clients exchanges the authorization code for a Access token to Access the Resource Server. This is the Authorization code flow as described by the OAuth 2 protocol. This works fine.
Now, I have the requirement to make a Controller of the MVC Client itself require Authentication. I can not find a tutorial for this.
I added
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
to my Startup.Auth.cs.
I assume, I need to setup the Options to Redirect to the Authorization Server. I can also set the Provider on the Options:
app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()
{
Provider = new OAuthBearerAuthenticationProvider()
});
But I am also stuck on implementing the events of the Provider.
Can anybody guide me in the right direction? Or are there any tutorials which might help me?
I ended up with a solution based on these two articles from Brock Allen:
http://brockallen.com/2013/10/24/a-primer-on-owin-cookie-authentication-middleware-for-the-asp-net-developer/
http://brockallen.com/2014/01/09/a-primer-on-external-login-providers-social-logins-with-owinkatana-authentication-middleware/
The fundemental idea is to register two authentication Middlewares. An active Cookie-Authentication and a passive OAuthBearer-Authentication. In Startup.Auth.cs they are added like this:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/ExternalLogin/Login"),
});
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ExternalBearer,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
});
You also add an ExternalLogin-Controller. Its Login-method has to redirect the user to the Login-page of your Authorization Server to get the authorization code. You have to supply a callback function where you will process the authorization code.
public async Task<ActionResult> Login(string returnUrl)
{
if (string.IsNullOrEmpty(returnUrl) && Request.UrlReferrer != null)
returnUrl = Server.UrlEncode(Request.UrlReferrer.PathAndQuery);
if (Url.IsLocalUrl(returnUrl) && !string.IsNullOrEmpty(returnUrl))
_returnUrl = returnUrl;
//callback function
_redirectUrl = Url.Action("AuthorizationCodeCallback", "ExternalLogin", null, Request.Url.Scheme);
Dictionary<string, string> authorizeArgs = null;
authorizeArgs = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"response_type", "code"}
,{"scope", "read"}
,{"redirect_uri", _redirectUrl}
// optional: state
};
var content = new FormUrlEncodedContent(authorizeArgs);
var contentAsString = await content.ReadAsStringAsync();
return Redirect("http://localhost:64426/oauth/authorize?" + contentAsString);
}
In your callback-function you exchange the authorization code for an access token (plus refresh token) challenge your passive OAuthBearer-authentication Middleware and signin with the Access token as your Cookie.
public async Task<ActionResult> AuthorizationCodeCallback()
{
// received authorization code from authorization server
string[] codes = Request.Params.GetValues("code");
var authorizationCode = "";
if (codes.Length > 0)
authorizationCode = codes[0];
// exchange authorization code at authorization server for an access and refresh token
Dictionary<string, string> post = null;
post = new Dictionary<string, string>
{
{"client_id", "0123456789"}
,{"client_secret", "ClientSecret"}
,{"grant_type", "authorization_code"}
,{"code", authorizationCode}
,{"redirect_uri", _redirectUrl}
};
var client = new HttpClient();
var postContent = new FormUrlEncodedContent(post);
var response = await client.PostAsync("http://localhost:64426/token", postContent);
var content = await response.Content.ReadAsStringAsync();
// received tokens from authorization server
var json = JObject.Parse(content);
_accessToken = json["access_token"].ToString();
_authorizationScheme = json["token_type"].ToString();
_expiresIn = json["expires_in"].ToString();
if (json["refresh_token"] != null)
_refreshToken = json["refresh_token"].ToString();
//SignIn with Token, SignOut and create new identity for SignIn
Request.Headers.Add("Authorization", _authorizationScheme + " " + _accessToken);
var ctx = Request.GetOwinContext();
var authenticateResult = await ctx.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalBearer);
ctx.Authentication.SignOut(DefaultAuthenticationTypes.ExternalBearer);
var applicationCookieIdentity = new ClaimsIdentity(authenticateResult.Identity.Claims, DefaultAuthenticationTypes.ApplicationCookie);
ctx.Authentication.SignIn(applicationCookieIdentity);
var ctxUser = ctx.Authentication.User;
var user = Request.RequestContext.HttpContext.User;
//redirect back to the view which required authentication
string decodedUrl = "";
if (!string.IsNullOrEmpty(_returnUrl))
decodedUrl = Server.UrlDecode(_returnUrl);
if (Url.IsLocalUrl(decodedUrl))
return Redirect(decodedUrl);
else
return RedirectToAction("Index", "Home");
}
I hope this is useful for someone who is implementing the OAuth authorization code flow in his MVC 5 application.
I used official sample MVC Implicit Client which I believe is the correct authentication flow for MVC application.
For authorization I used this getting started, especially the part about infinite loop when roles are specified [Authorize(Roles = "Foo,Bar")] and user is authenticated but doesn't own any of these.