Could not complete oAuth2.0 login - asp.net-core

I have implemented Aspnet.security.openidconnect.server with .net core 2.1 app. Now I want to test my authorization and for that I am making postman request. If I change the grant type to client_credentials then it works but I want to test complete flow, so I select grant type to Authorzation code and it starts giving error "Could not complete oAuth2.0 login.
Here is the code:
services.AddAuthentication(OAuthValidationDefaults.AuthenticationScheme).AddOAuthValidation()
.AddOpenIdConnectServer(options =>
{
options.AuthorizationEndpointPath = new PathString(AuthorizePath);
// Enable the token endpoint.
options.TokenEndpointPath = new PathString(TokenPath);
options.ApplicationCanDisplayErrors = true;
options.AccessTokenLifetime = TimeSpan.FromMinutes(5);
#if DEBUG
options.AllowInsecureHttp = true;
#endif
options.Provider.OnValidateAuthorizationRequest = context =>
{
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal))
{
context.Validate(context.RedirectUri);
}
return Task.CompletedTask;
};
// Implement OnValidateTokenRequest to support flows using the token endpoint.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject token requests that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsClientCredentialsGrantType() && !context.Request.IsPasswordGrantType()
&& !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
if (string.IsNullOrEmpty(context.ClientId))
{
context.Skip();
return Task.CompletedTask;
}
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal) &&
string.Equals(context.ClientSecret, Configuration["OpenIdServer:ClientSecret"], StringComparison.Ordinal))
{
context.Validate();
}
return Task.CompletedTask;
};
// Implement OnHandleTokenRequest to support token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let
// the OpenID Connect server handle the other grant types.
if (context.Request.IsClientCredentialsGrantType() || context.Request.IsPasswordGrantType())
{
//var identity = new ClaimsIdentity(context.Scheme.Name,
// OpenIdConnectConstants.Claims.Name,
// OpenIdConnectConstants.Claims.Role);
ClaimsIdentity identity = null;
if (context.Request.IsClientCredentialsGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.ClientId, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
else if (context.Request.IsPasswordGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.Username, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
// Add the mandatory subject/user identifier claim.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
// By default, claims are not serialized in the access/identity tokens.
// Use the overload taking a "destinations" parameter to make sure
// your claims are correctly inserted in the appropriate tokens.
identity.AddClaim("urn:customclaim", "value",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new Microsoft.AspNetCore.Authentication.AuthenticationTicket(
new ClaimsPrincipal(identity),
new Microsoft.AspNetCore.Authentication.AuthenticationProperties(),
context.Scheme.Name);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes(
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess);
context.Validate(ticket);
}
return Task.CompletedTask;
};
and here is the postman collection:
Now I am not sure that whether the issue is in my code or in postman collection? I think the callback url is creating some issue but I am not sure. Any help?
Update:
By visiing this page https://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-implementing-the-authorization-code-and-implicit-flows/ I have found the issue. I haven't handled authorization code flow in my code but I even don't want to. Is there any way I test my code with Resource owner password? I can't see this grant type in request form. In simple words I want postman to open login screen which is in Controller/Login/Index and I select my ssl Certificate and it generates a token for me?

hello i think that you have to add https://www.getpostman.com/oauth2/callback as the redirect_url in your server config, i don't think that your STS server will return tokens back to a non trusted url. that's why it works from your app but not from Postman

Related

OpenIdDict Degraded Mode with ClientCredentials flow

I'm following this blog about allowing OpenIDDict to wrap an alternative authentication provider but return a JWT token from OpenIDDict itself:
https://kevinchalet.com/2020/02/18/creating-an-openid-connect-server-proxy-with-openiddict-3-0-s-degraded-mode/
This is really about intercepting the Authorization Code flow rather than the Client Credentials flow, but it provides a good starting point.
Unfortunately it states that "we don't need to override the HandleTokenRequestContext", which is appropriate for the blog but not (as far as I know) for my use case.
I think I need to implement a custom HandleTokenRequestContext but when I do so, the code runs, no errors but the HTTP response is empty. No token is generated.
How should I properly intercept the Client Credentials flow so that I can call out to another provider to validate the credentials, get a result and include that in the custom claims that I need to add to the JWT?
Code below:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DbContext>(options =>
{
// Configure the context to use an in-memory store - probably not needed?
options.UseInMemoryDatabase(nameof(DbContext));
// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();
});
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<DbContext>();
})
.AddServer(options =>
{
options.SetTokenEndpointUris("/connect/token");
options
//.AllowRefreshTokenFlow()
.AllowClientCredentialsFlow();
// Register the signing and encryption credentials.
// options.AddDevelopmentEncryptionCertificate()
// .AddDevelopmentSigningCertificate();
//Development only
options
.AddEphemeralEncryptionKey()
.AddEphemeralSigningKey()
.DisableAccessTokenEncryption();
// Register scopes (i.e. the modes we can operate in - there may be a better way to do this (different endpoints?)
options.RegisterScopes("normal", "registration");
//TODO: Include Quartz for cleaning up old tokens
options.UseAspNetCore()
.EnableTokenEndpointPassthrough();
options.EnableDegradedMode(); //Activates our custom handlers as the only authentication mechansim, otherwise the workflow attempt to invoke our handler *after* the default ones have already failed
//the request
options.AddEventHandler<ValidateTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
//TODO: Check that the client Id is known
if (!string.Equals(context.ClientId, "client-1", StringComparison.Ordinal))
{
context.Reject(
error: Errors.InvalidClient,
description: "The specified 'client_id' doesn't match a known Client ID.");
return default;
}
return default;
}));
options.AddEventHandler<HandleTokenRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role);
identity.AddClaim(OpenIddictConstants.Claims.Subject, context.ClientId, OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken);
if (context.Request.Scope == "registration")
{
//TODO: Authenticate against BackOffice system to get it's token so we can add it as a claim
identity.AddClaim("backoffice_token", Guid.NewGuid().ToString(), OpenIddictConstants.Destinations.AccessToken);
}
else
{
//TODO: Authenticate against internal authentication database as normal
}
var cp = new ClaimsPrincipal(identity);
cp.SetScopes(context.Request.GetScopes());
context.Principal = cp;
//This doesn't work either
//context.SignIn(context.Principal);
//ERROR: When this exits the response is empty
return default;
}));
});
//.AddValidation(options =>
//{
// options.UseLocalServer();
// options.UseAspNetCore();
//});
services.AddControllers();
services.AddHostedService<CredentialLoader>();
}
In the end, I got this working with 3 changes:
In the HandleTokenRequestContext:
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
Not sure if this is essential or not. Then remove the
context.Principal = cp;
but re-add the
context.SignIn(context.Principal);

Multi-Factor Authentication is not working properly with ASP.NET Core 2.1 Identity

I've tried to implement a multi-factor authentication for my web application.
I'm able to generate an Authenticator-Key based on my logged-in user. Using Google Authenticator, I'm able to generate a code. The problem come up when I'm validating this code. (It's a time-based code)
For some reason, it seems like my _userManager.VerifyTwoFactorTokenAsync(...) doesn't work while the code is not expired, but as soon as the code change in the Google Authenticator app, the same "expired" code is now valid, and I'm now redirected and logged in.
This is my token generator code :
var user = await _userManager.GetUserAsync(User);
var authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (authenticatorKey == null)
{
await _userManager.ResetAuthenticatorKeyAsync(user);
authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
return View(new RegisterAuthenticatorViewModel { AuthenticatorKey = authenticatorKey });
And this is my code validation code :
var user = await _userManager.GetUserAsync(User);
var isValid = await _userManager.VerifyTwoFactorTokenAsync(user,
_userManager.Options.Tokens.AuthenticatorTokenProvider, model.Code);
if (!isValid)
{
ModelState.AddModelError(string.Empty, "Code is invalid");
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
return View("_Success");
Since the code is actually working, my problem is to why it's validating the code only after it expired on the Google Authenticator App.

Revoke All Refresh Tokens of User

I'm using password grant flow, with asp.net Identity.
I want to kill all refresh token for a user, every time a login its executed.
I need this to kill its "session" even when he sign in with a different device, like other pc, or smartphone.
So, how can I do it?
Can I just do a UserManager.UpdateSecurityStampAsync(user.Id);, or I need something else?
Thank you so much for your help!
Can I just do a UserManager.UpdateSecurityStampAsync(user.Id); or I need something else?
This is definitely possible. For that, simply tweak your token endpoint to ask Identity to validate the security stamp before returning a valid token response. Here's an example:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request) {
// ...
if (request.IsRefreshTokenGrantType()) {
// Retrieve the claims principal stored in the refresh token.
var info = await HttpContext.Authentication.GetAuthenticateInfoAsync(
OpenIdConnectServerDefaults.AuthenticationScheme);
// Retrieve the user profile and validate the
// security stamp stored in the refresh token.
var user = await _signInManager.ValidateSecurityStampAsync(info.Principal);
if (user == null) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The refresh token is no longer valid."
});
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The user is no longer allowed to sign in."
});
}
// Create a new authentication ticket, but reuse the properties stored
// in the refresh token, including the scopes originally granted.
var ticket = await CreateTicketAsync(request, user, info.Properties);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
// ...
}
Alternatively, you can also use OpenIddictTokenManager to revoke all the refresh tokens associated with a user:
foreach (var token in await manager.FindBySubjectAsync("[userid]", cancellationToken)) {
await manager.RevokeAsync(token, cancellationToken);
}

Azure mobile apps Custom + Facebook authentication with Xamarin.Forms

I'm working on a Xamarin Forms mobile app with .NET backend. I followed this guide and successfully set up custom authentications with one change in Startup.cs:
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
SigningKey = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY"),
ValidAudiences = new[] { Identifiers.Environment.ApiUrl },
ValidIssuers = new[] { Identifiers.Environment.ApiUrl },
TokenHandler = config.GetAppServiceTokenHandler()
});
Without "if (string.IsNullOrEmpty(settings.HostName))". Otherwise I am always getting unauthorized for all requests after login.
Server project:
Auth controller
public class ClubrAuthController : ApiController
{
private readonly ClubrContext dbContext;
private readonly ILoggerService loggerService;
public ClubrAuthController(ILoggerService loggerService)
{
this.loggerService = loggerService;
dbContext = new ClubrContext();
}
public async Task<IHttpActionResult> Post(LoginRequest loginRequest)
{
var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Email == loginRequest.username);
if (user == null)
{
user = await CreateUser(loginRequest);
}
var token = GetAuthenticationTokenForUser(user.Email);
return Ok(new
{
authenticationToken = token.RawData,
user = new { userId = loginRequest.username }
});
}
private JwtSecurityToken GetAuthenticationTokenForUser(string userEmail)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userEmail)
};
var secretKey = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY");
var audience = Identifiers.Environment.ApiUrl;
var issuer = Identifiers.Environment.ApiUrl;
var token = AppServiceLoginHandler.CreateToken(
claims,
secretKey,
audience,
issuer,
TimeSpan.FromHours(24)
);
return token;
}
}
Startup.cs
ConfigureMobileAppAuth(app, config, container);
app.UseWebApi(config);
}
private void ConfigureMobileAppAuth(IAppBuilder app, HttpConfiguration config, IContainer container)
{
config.Routes.MapHttpRoute("ClubrAuth", ".auth/login/ClubrAuth", new { controller = "ClubrAuth" });
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
SigningKey = Environment.GetEnvironmentVariable("WEBSITE_AUTH_SIGNING_KEY"),
ValidAudiences = new[] { Identifiers.Environment.ApiUrl },
ValidIssuers = new[] { Identifiers.Environment.ApiUrl },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
Client project:
MobileServiceUser user = await MobileClient.LoginAsync(loginProvider, jtoken);
Additionally I configured Facebook provider in azure portal like described here.
But it works only when I comment out app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions(){...}); in Startup.cs.
What I am missing to make both types of authentication works at the same time?
Since you have App Service Authentication/Authorization enabled, that will already validate the token. It assumes things about your token structure, such as having the audience and issuer be the same as your app URL (as a default).
app.UseAppServiceAuthentication() will also validate the token, as it is meant for local development. So in your example, the token will be validated twice. Aside from the potential performance impact, this is generally fine. However, that means the tokens must pass validation on both layers, and I suspect that this is not the case, hence the error.
One way to check this is to inspect the tokens themselves. Set a breakpoint in your client app and grab the token you get from LoginAsync(), which will be part of that user object. Then head to a service like http://jwt.io to see what the token contents look like. I suspect that the Facebook token will have a different aud and iss claim than the Identifiers.Environment.ApiUrl you are configuring for app.UseAppServiceAuthentication(), while the custom token probably would match it since you're using that value in your first code snippet.
If that holds true, than you should be in a state where both tokens are failing. The Facebook token would pass the hosted validation but fail on the local middleware, while the custom token would fail the hosted validation but pass the local middleware.
The simplest solution here is to remove app.UseAppServiceAuthentication() when hosting in the cloud. You will also need to make sure that your call to CreateToken() uses the cloud-based URL as the audience and issuer.
For other folks that find this issue
The documentation for custom authentication can be found here.
A general overview of App Service Authentication / Authorization can be found here.
The code you reference is only for local deployments. For Azure deployments, you need to turn on App Service Authentication / Authorization - even if you don't configure an auth provider (which you wouldn't in the case of custom auth).
Check out Chapter 2 of my book - http://aka.ms/zumobook

Thinktecture IdentityServer v3 LogOut for Implicit flow

How do I get the id_token for the implicit token to pass in the id_token hint for logout for implicit flow or is there another way? I have the end point /connect/endsession?
id_token_hint=
Not sure how I get the id_token from the implict flow all I get is a access_token and expiration. Is there a setting in IdSvr?
There's three components to this.
First ensure you're requesting an id_token from Identity Server when you're configuring the OIDC authentication in your Startup.cs (as mentioned by #leastprivilege above):
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44301/",
...
ResponseType = "id_token token", //(Here's where we request id_token!)
Secondly, using the OIDC notifications & after the security token is validated you add the id_token to your user's claims:
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var nid = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
Constants.ClaimTypes.GivenName,
Constants.ClaimTypes.Role);
// get userinfo data
var userInfoClient = new UserInfoClient(
new Uri(n.Options.Authority + "/" + Constants.RoutePaths.Oidc.UserInfo),
n.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
userInfo.Claims.ToList().ForEach(ui => nid.AddClaim(new Claim(ui.Item1, ui.Item2)));
// keep the id_token for logout (**This bit**)
nid.AddClaim(new Claim(Constants.TokenTypes.IdentityToken, n.ProtocolMessage.IdToken));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
},
Finally, on the redirect for signout (also a notification event) you add the id_token to the Protocol Message:
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(Constants.TokenTypes.IdentityToken);
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
You'll also need to ensure you setup the PostLogoutRedirectUris on the client within Identity Server:
new Client
{
Enabled = true,
ClientName = "(MVC) Web App",
ClientId = "mvc",
Flow = Flows.Implicit,
PostLogoutRedirectUris = new List<string>
{
"https://localhost:44300/" //(** The client's Url**)
}
}
That will ensure you give the user an option to return to the authorised client when they log out :)
All of this is pretty much as per the MVC Sample at https://identityserver.github.io/Documentation/docsv2/overview/mvcGettingStarted.html
Bit more than you asked for but hopefully that helps anyone else who's trying to figure it out too :)
To get an id_token, you have to ask for it. Use response_type=id_token token
Have you tried this?
ASP.Net Identity Logout
It should create the id token hint automatically