Parameterize the LoginUrl in IdentityServer? - asp.net-core

I have been trying to add a parameter in the Login URL. E.g.
services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/{culture}/account/login";
});
Is it possible to have such types of parameterized URLs? Also, how would I pass this culture from my (JavaScript) client application? What other options do I have to retain this culture (e.g. as a query string parameter or route data)?
Thanks

After looking into this I managed to hack something that works. I used query string to pass the parameter but you could do it with url.
var paramName = "culture";
app.Use(async (context, next) =>
{
await next.Invoke();
if (context.Request.Path.Value?.Contains("/connect/authorize") ?? false && context.Response.StatusCode == 302 && context.Request.Query[paramName].Count == 1)
{
var location = context.Response.Headers["Location"].ToString();
var tenant = context.Request.Query[paramName].ToString();
var newUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(location, "tenant", tenant);
context.Response.Redirect(newUrl);
}
});
I send the param from owin UseOpenIdConnectAuthentication and you can add a parameter with
RedirectToIdentityProvider = (a) =>
{
a.ProtocolMessage.Parameters.Add("culture", "en");
return Task.FromResult(0);
},
I am sure you could add the param to all other providers.
NOTE: Don't use this for any secure info just stuff that are cosmetic.

You can pass dynamic values from client application using client oidc externParam.
eg; for oidc-client library(https://www.npmjs.com/package/oidc-client)
// client side code
manager = new UserManager({
authority: localhost:5000 ,
client_id: xxx,
redirect_uri: xx,
response_type: 'code',
scope: 'openid profile .....',
//etc add more settings you need
};);
//initialize connection with identity server
//send settings to authorize endpoint (localhost:5000/connect/authorize)
this.manager.signinRedirect(
{
extraQueryParams: {abc:1} // you can add any number of custom key: value pairs
}
);
but now how to create dynamic options.UserInteraction.LoginUrl based on extraQueryParams received by identity Server at authorize endpoint?
Example:
I have two login pages login1 and login2; not on identity server but at some other url for instance
localhost:4200/login1
localhost:4200/login2
now based on the extraQueryParams received you can add show either of these login pages using AuthorizeInteractionResponseGenerator.
//server side code
//startup.cs
services.AddIdentityServer()
.AddAuthorizeInteractionResponseGenerator<YourCustomAuthorizeEndpointResponseGenerator>();
//make new class
public class YourCustomAuthorizeEndpointResponseGenerator : AuthorizeInteractionResponseGenerator //extending default res generator; you can rewrite also using IAuthorizeInteractionResponseGenerator
{
public YourCustomAuthorizeEndpointResponseGenerator(ISystemClock clock,
ILogger<AuthorizeInteractionResponseGenerator> logger, IConsentService consent,
IProfileService profile,
IHttpContextAccessor httpContextAccessor,
IdentityServerOptions identityServerOptions, // Will be auto injected by identity server; use this to override loginUrl
)
: base(clock, logger, consent, profile)
{
if (httpContextAccessor.HttpContext.Request.QueryString.Value.ToString().Contains("abc=1"))
{
identityServerOptions.UserInteraction.LoginUrl = localhost:4200/login1;
}
else
{
identityServerOptions.UserInteraction.LoginUrl = localhost:4200/login2;
}
}
}
all redirection and callbacks will be taken care by identity server based upon the redirect_uri setting passed from client side.

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);

ASP.NET Core Authentication : Bearer Token for an action and Windows Authentication for another in the same controller

What I want to do is to propose in my API different kind of endpoint that can be accessed with a Windows Authentication or JWT Bearer authentication.
In Startup.cs --> Configure, authentication is configured like this to allow Bearer authentication with desired parameters :
// Add JWT Bearer
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(tokenManagement.Secret)),
ValidIssuer = tokenManagement.Issuer,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = false,
ClockSkew = TimeSpan.FromSeconds(tokenManagement.ClockSkewSeconds)
};
});
And to protect my endpoints i've tried something like this :
// THIS WAY I CAN LIMIT TO WINDOWS CREDENTIAL
[Authorize]
[HttpGet("[action]")]
public IActionResult AuthorizeWindowsUser()
{
var user = HttpContext.User;
if (user.GetType() == typeof(System.Security.Principal.WindowsPrincipal))
{
return Ok();
}
return Unauthorized();
}
// THIS WAY I CAN LIMIT TO JWT
[Authorize]
[HttpGet("[action]")]
public IActionResult AuthorizeLoginUser()
{
var user = HttpContext.User;
if (user.GetType() == typeof(System.Security.Claims.ClaimsPrincipal))
{
return Ok();
}
return Unauthorized();
}
It is working, but my questions will be :
Is it something that seems logical ? My goals is to protect the endpoints that deliver a User Token. One endpoint will be protected with a JWT Token (Refresh token) and a specific role, one will be protected with Windows Credential.
Am i missing something ? When i was using just Windows Authentication i used to set services.AddAuthentication(IISDefaults.AuthenticationScheme); in my Startup.cs --> Configure it seems that if is not necessary to work in the described implementation above but i don't really know what does this line and if it is necessary or not (btw it seems no).
Is there a smarter/prettier way to check the type of user ? Maybe something like a custom attributes
Thanks for your advices !
it seems that if is not necessary to work
That's because the WebHost.CreateDefaultBuilder(args) does it for you. The method will invoke .UseIISIntegration(); and add related services behind the scenes. For more details, see
Source code of invoking UseIISIntegration() and Source of UseIISIntegration() method.
Is there a smarter/prettier way to check the type of user ? Maybe something like a custom attributes
You don't have to check if( user.GetType() == typeof(System.Security.Principal.WindowsPrincipal) {return ok;} return Unauthorized() manually within the action method. Use the built-in AuthorizeAttribute instead :
[Authorize(AuthenticationSchemes = "Windows")]
[HttpGet("[action]")]
public IActionResult AuthorizeWindowsUser()
{
var user = HttpContext.User;
if (user.GetType() == typeof(System.Security.Principal.WindowsPrincipal))
{
return Ok();
}
return Unauthorized();
return Ok(); // only users who are authorized with the Windows scheme can access this method
}
The same goes for Jwt Bearer. Since you've configured the JwtBearerDefaults.AuthenticationScheme as the default authentication scheme, you can omit the AuthenticationSchemes parameter when using the [Authorize()] attribute annotation:
[Authorize]
[HttpGet("[action]")]
public IActionResult AuthorizeLoginUser()
{
var user = HttpContext.User;
if (user.GetType() == typeof(System.Security.Claims.ClaimsPrincipal))
{
return Ok();
}
return Unauthorized();
return Ok(); // only users who are authorized with the default scheme can access this method
}

Could not complete oAuth2.0 login

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

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

Owin authentication cookie not visible subsequent to the first request

MVC 5 app with Microsoft Owin v3 authenticating users via Windows Azure Active Directory. I have a login process that I don't fully understand. I expect to see a cookie on every request by I only see one on the landing page after sign-in. After navigating to another controller the cookie disappears yet the session appears to be authenticated correctly. Does anyone know how this is working? Here is my sign on logic...I don't see any cookie with the expiry time set in any browser. I see .AspNet.Cookies, __RequestVerificationToken and 2 cookies associated with a support utility. Removing any of these using Firebug has no effect on the user session and I remain logged in.
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/", IsPersistent = true, AllowRefresh = true, ExpiresUtc = DateTime.UtcNow.AddMinutes(20) },
OpenIdConnectAuthenticationDefaults.AuthenticationType
);
And here is the startup logic taken from an online example...
public void ConfigureAuth(IAppBuilder app)
{
//TODO: Use the Ioc container to get this but need to check if the kernel has been created before this runs
string applicationClientId = ConfigurationManager.AppSettings.Get(ConfigurationConstants.AppSettings.AzureApplicationClientId);
//fixed address for multitenant apps in the public cloud
string authority = "https://login.windows.net/common/";
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { CookieDomain = "example.com" });
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = applicationClientId,
Authority = authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
ValidateIssuer = false,
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = (context) =>
{
// This ensures that the address used for sign in and sign out is picked up dynamically from the request
// this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
// Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl;
//This will need changing to the web site home page once it is live
context.ProtocolMessage.PostLogoutRedirectUri = "http://www.example.com";
return Task.FromResult(0);
},
// we use this notification for injecting our custom logic
SecurityTokenValidated = (context) =>
{
// retriever caller data from the incoming principal
string issuer = context.AuthenticationTicket.Identity.FindFirst("iss").Value;
string UPN = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).Value;
string tenantId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
//Todo - fetch the tenant info
//if ((db.Tenants.FirstOrDefault(a => ((a.IdentityProvider == issuer) && (a.ActiveDirectoryTenantId == tenantId))) == null))
// // the caller wasn't from a trusted issuer throw to block the authentication flow
// throw new SecurityTokenValidationException();
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.OwinContext.Response.Redirect("/Home/Error");
context.HandleResponse(); // Suppress the exception
return Task.FromResult(0);
}
}
});
}