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)
Related
I have an Angular 9 web application connected via the oidc-client to Identity Server 4 and an API using Implicit flow. When I get the authenticated user I can see several claims I want for the site such as the email address, the user name or the role.
I'm now trying to do the exact same thing using the password flow and I'm getting only the sub claim back - note this is the first time I use it and therefore it may not be right, but in essence, below would be the call I'm performing (using this time angular-oauth2-oidc through my ionic app) - for simplicity and for testing purposes I'm using postman to illustrate this:
I have modified my client to allow the profile scope without any luck and also I'm getting a different type of response and claim processing targetting the same user using the same configuration on IS4:
My question is, is there anything special I need to set up in my client when I use the password flow to get the claims back or do I need to modify the profile service to include them all the time? I would have imagined when you have access to different scopes and they have issued claims you should get them back but I'm not sure if I'm missing something fundamental here.
My client's config:
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "web",
ClientName = "Web Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedScopes = new List<string> { "openid", "profile", "myapi" },
RedirectUris = new List<string> {
"http://<base-url>/auth-callback",
"http://<base-url>/silent-renew-callback",
},
PostLogoutRedirectUris = new List<string> {"http://<base-url>"},
AllowedCorsOrigins = new List<string> {"http://<base-url>"},
AllowAccessTokensViaBrowser = true,
RequireConsent = false,
AlwaysSendClientClaims = true,
AlwaysIncludeUserClaimsInIdToken = true,
},
new Client
{
ClientId = "mobile",
ClientName = "Mobile Client",
ClientSecrets = { new Secret("t8Xa)_kM6apyz55#SUv[[Cp".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AllowedScopes = new List<string> { "openid", "mobileapp", "myapi" },
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 3600,
IdentityTokenLifetime = 3600,
UpdateAccessTokenClaimsOnRefresh = false,
SlidingRefreshTokenLifetime = 30,
AllowOfflineAccess = true,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true
}
};
}
}
Any tips are highly appreciated. Many thanks!
UPDATE: Since ROPC flow is being deprecated in oauth 2.1 (https://fusionauth.io/blog/2020/04/15/whats-new-in-oauth-2-1) I decided to move everything to the code flow + PKCE mechanism.
Password grant is an OAuth grant and is to obtain an access token. And what you see as a result of password grant is an access token. access token does not contain any information about the user itself besides their ID (sub claim).
But Implicit grant you use is OpenId Grant. You use oidc client lib and use "openid", "profile" on client - AllowedScopes. What you get in result in an id token. This token authenticates the user to the application and contains user info.
Read more about tokens here.
And this is a very good post which Diagrams of All The OpenID Connect Flows
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 access token URL working with IdentityServer3. The Server is configured the following way:
var options = new IdentityServerOptions
{
LoggingOptions = new LoggingOptions
{
WebApiDiagnosticsIsVerbose = true,
EnableWebApiDiagnostics = true,
EnableHttpLogging = true,
EnableKatanaLogging= true
},
Factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get())
.UseInMemoryUsers(Users.Get()),
RequireSsl = false,
EnableWelcomePage = false,
};
app.UseIdentityServer(options);
The client configuration:
new Client
{
Enabled = true,
ClientName = "JS Client",
ClientId = "js",
Flow = Flows.Implicit,
RedirectUris = new List<string>
{
"http://localhost:56522"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:56522"
},
AllowAccessToAllScopes = true
}
Trying to POST the following HTTP request to token endpoint:
Content-Type:application/x-www-form-urlencoded
grant_type:password
redirect_uri:http://localhost:56522
client_id:js
username:bob
password:secret
scope:api
I get Invalid client error message and log shows:
Action returned 'IdentityServer3.Core.Results.TokenErrorResult'', Operation=ReflectedHttpActionDescriptor.ExecuteAsync
Any ideas what do I still miss?
Your request is using the password grant type, which is the OAuth Resource Owner flow, but your client is configured to use the OpenID Connect Implicit flow.
Either change your client configuration to use the Resource Owner flow, or change your request to be a valid OpenID Connect request.
For example: GET /connect/authorize?client_id=js&scope=openid api&response_type=id_token token&redirect_uri=http://localhost:56522&state=abc&nonce=xyz. This will take you to a login page.
Or better yet, use a JavaScipt library like #Jenan suggested, such as the IdentityModel oidc-client which handles these requests for you.
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
Am currently developing an Authorization server using Owin, Oauth, Claims.
Below is my Oauth Configuration and i have 2 questions
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(1000),
Provider = new AuthorizationServerProvider()
//RefreshTokenProvider = new SimpleRefreshTokenProvider()
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
If the token is expired and user accessing using the expired token user is getting 401(unAuthorized).Checking using Fiddler.
How can i send a customized message to an user stating your token as expired. Which function or module i need to override.
and my another quesiton is What is the use of the below line ?
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
Do i really need this to implement because when i checked it still works without the above line. Any security violation ?
You can't directly customize the behavior for expired tokens but you can do that with a custom middleware.
First override the AuthenticationTokenProvider so that you can intercept the authentication ticket before it is discarded as expired.
public class CustomAuthenticationTokenProvider : AuthenticationTokenProvider
{
public override void Receive(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
if (context.Ticket != null &&
context.Ticket.Properties.ExpiresUtc.HasValue &&
context.Ticket.Properties.ExpiresUtc.Value.LocalDateTime < DateTime.Now)
{
//store the expiration in the owin context so that we can read it later a middleware
context.OwinContext.Set("custom.ExpriredToken", true);
}
}
}
and configure it in the Startup along with a small custom middleware
using AppFunc = System.Func<System.Collections.Generic.IDictionary<string, object>, System.Threading.Tasks.Task>;
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
AccessTokenProvider = new CustomAuthenticationTokenProvider()
});
//after the request has been authenticated or not
//check for our custom env setting and act accordingly
app.Use(new Func<AppFunc, AppFunc>(next => (env) =>
{
var ctx = new OwinContext(env);
if (ctx.Get<bool>("custom.ExpriredToken"))
{
//do wathever you want with the response
ctx.Response.StatusCode = 401;
ctx.Response.ReasonPhrase = "Token exprired";
//terminate the request with this middleware
return Task.FromResult(0);
}
else
{
//proceed with the rest of the middleware pipeline
return next(env);
}
}));
If you have noticed I've placed the custom middleware after the call to UseOAuthBearerAuthentication and this is important and stems from the answer to your second question.
The OAuthBearerAuthenticationMidlleware is responsible for the authentication but not for the authorization. So it just reads the token and fills in the information so that it can be accessed with IAuthenticationManager later in the pipeline.
So yes, with or without it all your request will come out as 401(unauthorized), even those with valid tokens.