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
Related
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.
I'm trying to use my own MSAL code to work together. Developed with .NET Core 5 MVC.
I have similar problem as I found in below link. But I just don't know how to make it work with the proposed answer. Or in other words, I'm still confuse how this integration is done.
[It is mandatory to use the login component in order to use the other components]It is mandatory to use the login component in order to use the other components
[Quickstart for MSAL JS]https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/main/samples/examples/simple-provider.html
I also have read following article too:
[Simple Provider Example]https://github.com/microsoftgraph/microsoft-graph-toolkit/blob/main/samples/examples/simple-provider.html
[A lap around microsoft graph toolkit day 7]https://developer.microsoft.com/en-us/office/blogs/a-lap-around-microsoft-graph-toolkit-day-7-microsoft-graph-toolkit-providers/
is there someone can pointing to me more details explanation about how to archive this.
Can someone explains further below response further. How to do it. Where should I place the code and how to return AccessToken to SimpleProvider?
Edited:
Update my question to be more precise to what I want besides on top of the question. Below is the code I used in Startup.cs to automatically trigger pop up screen when user using the web app. When using the sample provided, it is always cannot get access token received or userid data. Question 2: How to save or store token received in memory or cache or cookies for later use by ProxyController and its classes.
//Sign in link under _layouts.aspx
<a class="nav-link" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a>
// Use OpenId authentication in Startup.cs
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
// Specify this is a web app and needs auth code flow
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAd", options);
options.Prompt = "select_account";
options.Events.OnTokenValidated = async context =>
{
var tokenAcquisition = context.HttpContext.RequestServices
.GetRequiredService<ITokenAcquisition>();
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(async (request) =>
{
var token = await tokenAcquisition
.GetAccessTokenForUserAsync(GraphConstants.Scopes, user: context.Principal);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
})
);
// Get user information from Graph
try
{
var user = await graphClient.Me.Request()
.Select(u => new
{
u.DisplayName,
u.Mail,
u.UserPrincipalName,
u.MailboxSettings
})
.GetAsync();
context.Principal.AddUserGraphInfo(user);
}
catch (ServiceException)
{
}
// Get the user's photo
// If the user doesn't have a photo, this throws
try
{
var photo = await graphClient.Me
.Photos["48x48"]
.Content
.Request()
.GetAsync();
context.Principal.AddUserGraphPhoto(photo);
}
catch (ServiceException ex)
{
if (ex.IsMatch("ErrorItemNotFound") ||
ex.IsMatch("ConsumerPhotoIsNotSupported"))
{
context.Principal.AddUserGraphPhoto(null);
}
}
};
options.Events.OnAuthenticationFailed = context =>
{
var error = WebUtility.UrlEncode(context.Exception.Message);
context.Response
.Redirect($"/Home/ErrorWithMessage?message=Authentication+error&debug={error}");
context.HandleResponse();
return Task.FromResult(0);
};
options.Events.OnRemoteFailure = context =>
{
if (context.Failure is OpenIdConnectProtocolException)
{
var error = WebUtility.UrlEncode(context.Failure.Message);
context.Response
.Redirect($"/Home/ErrorWithMessage?message=Sign+in+error&debug={error}");
context.HandleResponse();
}
return Task.FromResult(0);
};
})
// Add ability to call web API (Graph)
// and get access tokens
.EnableTokenAcquisitionToCallDownstreamApi(options =>
{
Configuration.Bind("AzureAd", options);
}, GraphConstants.Scopes)
// Add a GraphServiceClient via dependency injection
.AddMicrosoftGraph(options =>
{
options.Scopes = string.Join(' ', GraphConstants.Scopes);
})
// Use in-memory token cache
// See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization
.AddInMemoryTokenCaches();
Since you are using MVC, I recommend using the ProxyProvider over the Simple Provider.
SimpleProvider - useful when you have existing authentication on the client side (such as Msal.js)
ProxyProvider - useful when you are authenticating on the backend and all graph calls are proxied from the client to your backend.
This .NET core MVC sample might help - it is using the ProxyProvider with the components
Finally, I have discovered how to do my last mile bridging for these two technology.
Following are the lines of the code that I have made the changes. Since I'm using new development method as oppose by MSAL.NET, a lot of implementation has been simplified, so many of examples or article out there, may not really able to use it directly.
Besides using links shared by #Nikola and me above, you also can try to use below
https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/tree/master/
to consolidate to become your very own solution. Below are the changes I have made to make it worked.
Change in Startup.cs class
// Add application services. services.AddSingleton<IGraphAuthProvider, GraphAuthProvider>(); //services.AddSingleton<IGraphServiceClientFactory, GraphServiceClientFactory>();
Change in ProxyController.cs class
private readonly GraphServiceClient _graphClient;
public ProxyController(IWebHostEnvironment hostingEnvironment, GraphServiceClient graphclient)
{
_env = hostingEnvironment;
//_graphServiceClientFactory = graphServiceClientFactory;
_graphClient = graphclient;
}
Change in ProcessRequestAsync method under ProxyController.cs
//var graphClient = _graphServiceClientFactory.GetAuthenticatedGraphClient((ClaimsIdentity)User.Identity);
var qs = HttpContext.Request.QueryString;
var url = $"{GetBaseUrlWithoutVersion(_graphClient)}/{all}{qs.ToUriComponent()}";
var request = new BaseRequest(url, _graphClient, null)
{
Method = method,
ContentType = HttpContext.Request.ContentType,
};
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 am trying to implement OAuth to one of my companies' projects and can't resolve the following problem.
We used IdentityServer4 for implementing our own Authorization Server, which works fine so far. The resource I want to protect with OAuth is a WebApi utilizing Swagger/Swashbuckle.
I followed the IdentityServer4 QuickStartExamples to configure the server and this tutorial [Secure Web APIs with Swagger, Swashbuckle, and OAuth2 (part 2)](http://knowyourtoolset.com/2015/08/secure-web-apis-with-swagger-swashbuckle-and-oauth2-part-2 for configuring Swagger/Swashbuckle).
I have a dummy-action which does nothing else than returning a string, that works as expected.
When I decorate the action with [Authorize], a little red icon appears in swagger-ui, indicating that I have to log in to access this method. The Login process works fine: I am redirected to the Quickstart-UI, can login with the testuser "Bob", and I am redirected to swagger-ui after a successful login.
The problem: After the successful login, I still get an 401 error, stating "Authorization has been denied for this request."
I can see that a bearer token is returned by my IdentityServer in swagger-ui, so I guess this part working fine and the problem seems to be swagger/swashbuckle.
Is there maybe anything else I have to do with the token? In the tutorials I read so far, the swagger config is modified as I did it (see below) and that's it, so I guess swagger/swashbuckle should handle this - but maybe I miss out something?
SwaggerConfig.cs:
c.OAuth2("oauth2")
.Description("OAuth2 Implicit Grant")
.Flow("implicit") //also available: password, application (=client credentials?)
.AuthorizationUrl("http://localhost:5000/connect/authorize")
.TokenUrl("http://localhost:5000/connect/token")
.Scopes(scopes =>
{
scopes.Add("My.Web.Api", "THE Api");
});
// etc. .....
c.OperationFilter<AssignOAuth2SecurityRequirements>();
// etc. .....
c.EnableOAuth2Support(
clientId: "swaggerui",
clientSecret: "secret",
realm: "dummyrealm",
appName: "Swagger UI"
);
Filter for Authorize Attribute in SwaggerConfig.cs:
public class AssignOAuth2SecurityRequirements : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
// Determine if the operation has the Authorize attribute
var authorizeAttributes = apiDescription
.ActionDescriptor.GetCustomAttributes<AuthorizeAttribute>();
if (!authorizeAttributes.Any())
return;
// Initialize the operation.security property
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
// Add the appropriate security definition to the operation
var oAuthRequirements = new Dictionary<string, IEnumerable<string>>
{
{ "oauth2", new [] { "My.Web.Api" } }
};
operation.security.Add(oAuthRequirements);
}
}
IdentityServer api config:
new ApiResource("My.Web.Api", "THE Api")
IdentityServer client config:
new Client
{
ClientId = "swaggerui",
ClientName = "Swagger UI",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
AllowedCorsOrigins = { "http://localhost:5858" },
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { "http://localhost:5858/swagger/ui/o2c-html" },
PostLogoutRedirectUris = { "http://localhost:5858/swagger/ui/o2c-html" },
AllowedScopes =
{
"My.Web.Api"
}
Screenshot of redirection after login:
When using .NET Core (but it would appear that this question is for .NET Framework) I also encountered this same problem. It was solved by ensuring that in the Configure method of Startup you have UseAuthentication before UseAuthorization
(source https://learn.microsoft.com/en-us/aspnet/core/grpc/authn-and-authz?view=aspnetcore-3.1)
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.