Redirect to IdP with added querystring parameter - c#

I'm using IdentityServer4 and have configured an OpenId Connect provider. What I want to do is pass in a username to the provider as part of the querystring so that the provider pre-fills in the username field. I have both ADFS and Azure AD providers and would like this functionality to work with both. Is this possible and if so how?
In the Challenge method on ExternalController I've added what I think should work but it doesn't do anything:
[HttpGet]
public async Task<IActionResult> Challenge(string provider, string returnUrl, string user)
{
if (string.IsNullOrEmpty(returnUrl)) returnUrl = "~/";
if (Url.IsLocalUrl(returnUrl) == false && _interaction.IsValidReturnUrl(returnUrl) == false)
{
throw new Exception("invalid return URL");
}
if (AccountOptions.WindowsAuthenticationSchemeName == provider)
{
return await ProcessWindowsLoginAsync(returnUrl);
}
else
{
var props = new AuthenticationProperties
{
RedirectUri = Url.Action(nameof(Callback)),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", provider },
{ "login_hint", user }
}
};
return Challenge(props, provider);
}
}

You can achieve what you're looking for using the OnRedirectToIdentityProvider property of the OpenIdConnectEvents class:
Invoked before redirecting to the identity provider to authenticate. This can be used to set ProtocolMessage.State that will be persisted through the authentication process. The ProtocolMessage can also be used to add or customize parameters sent to the identity provider.
You hook into this process via the AddOpenIdConnect function, which is called when using services.AddAuthentication in Startup.ConfigureServices. Here's an example of what this might look like for your requirements:
services
.AddAuthentication(...)
.AddOpenIdConnect(options =>
{
...
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = ctx =>
{
if (ctx.HttpContext.Request.Query.TryGetValue("user", out var stringValues))
ctx.ProtocolMessage.LoginHint = stringValues[0];
return Task.CompletedTask;
}
};
});
Most of this is just the boilerplate code for adding authentication, OIDC and registering an event-handler for the event detailed above. The most interesting part is this:
if (ctx.HttpContext.Request.Query.TryGetValue("user", out var stringValues))
ctx.ProtocolMessage.LoginHint = stringValues[0];
As your Challenge action from your question gets user from a query-string parameter, the code above reads out the user query-string parameter from the request (there could be more than one, which is why we have a StringValues here) and sets it as the LoginHint property, if it's found.
Note: I've tested this with https://demo.identityserver.io (which works, of course).

Related

How to validate if user exist inside IdentityServer4 after being authenticated from External Provider?

I'm trying to find a proper way where I can inject a service to validate if user exists or registered in my application after being successfully authenticated from an external identity provider like Azure Active Directory. What I want to do is to redirect user to a custom error page or display an Unauthorized message if his account is not yet registered in my application.
I tried utilizing the IProfileService interface but it seems not the right way to go.
Here is my Startup.cs setup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddTestUsers(Config.GetUsers())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients()) // Client was configured with RequireConsent = false, EnableLocalLogin = false,
.AddProfileService<ProfileService>()
.Services.AddTransient<IUserRepository,UserRepository>();
services.AddAuthentication()
.AddOpenIdConnect("AAD", "Azure Active Directory", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.microsoftonline.com/MyTenant";
options.ClientId = "MyClientId";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
options.GetClaimsFromUserInfoEndpoint = true;
});
}
public class ProfileService : IProfileService
{
private readonly IUserRepository _userRepository;
public ProfileService(IUserRepository userRepository)
{
_userRepository = userRepository
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = _userRepository.FindByUser(context.Subject.Identity.Name);
// This will display HTTP 500 instead of 401
if(user == null) throw new UnauthorizedAccessException("You're not registered");
// I add custom claims here
return Task.FromResult(0);
}
public Task IsActiveAsync(IsActiveContext context) => Task.FromResult(0);
}
Is there any available service or interface I can use where I can inject my user validation as well as allowing me to inject my user repository in that service? Is it possible to inject this kind of process inside IdentityServer4? Can someone point me in the right direction to accomplish my goal using IdentityServer4?
Note: Lets assume I have SPA web app and I have my own separate registration mechanism. I don't want to redirect back to my SPA if user doesn't exist and handle it inside IdentityServer4 instead. Btw, some of the code above are not included for brevity.
The IdentityServer4 QuickStart UI is configured to auto-provision local user accounts when signing-in through an external provider. That's all handled in ExternalController.Callback:
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
// this might be where you might initiate a custom workflow for user registration
// in this sample we don't show how that would be done, as our sample implementation
// simply auto-provisions new external user
user = AutoProvisionUser(provider, providerUserId, claims);
}
In your situation, you can perform whatever logic you need to perform instead of calling AutoProvisionUser. As this is a regular MVC action that's being executed, you have the ability to inject your own classes into ExternalController's constructor or into Callback itself (using [FromServices]). Here's a rough idea of the changes you might want to make:
public async Task<IActionResult> Callback([FromServices] IUserRepository userRepository)
{
...
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = FindUserFromExternalProvider(result);
if (user == null)
{
// We don't have a local user.
return RedirectToAction("SomeAction", "SomeController");
}
...
}
You can write your custom logic in ExternalLoginCallback function in in AccountController if you are using ASP.NET Identity . After getting JWT token issued from Azure AD , you can decode the token ,get the user claims such as email/name :
if (remoteError != null)
{
ErrorMessage = $"Error from external provider: {remoteError}";
return RedirectToAction(nameof(Login));
}
// read external identity from the temporary cookie
var aadResult1 = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (aadResult1?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var externalUser = aadResult1.Principal;
if (externalUser == null)
{
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var claims = externalUser.Claims.ToList();
// try to determine the unique id of the external user - the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject);
if (userIdClaim == null)
{
userIdClaim = claims.FirstOrDefault(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier");
}
if (userIdClaim == null)
{
throw new Exception("Unknown userid");
}
Then you can write your service implement/logic in database to confirm whether user is already in database . If yes , login in user;if no , redirect user to confirmation/register view . Something like:
// Sign in the user with this external login provider if the user already has a login.
var result = await _signInManager.ExternalLoginSignInAsync("YourProvider", userIdClaim.Value, isPersistent: false, bypassTwoFactor: true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in with {Name} provider.", "YourProvider");
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
return RedirectToLocal(returnUrl);
}
if (result.IsLockedOut)
{
return RedirectToAction(nameof(Lockout));
}
else
{
// If the user does not have an account, then ask the user to create an account.
ViewData["ReturnUrl"] = returnUrl;
ViewData["LoginProvider"] = "YourProvider";
var email = claims.FirstOrDefault(x => x.Type == ClaimTypes.Upn).Value;
return View("ExternalLogin", new ExternalLoginViewModel { Email = email });
}
It depends on you for how to link AD user to local database user .use Azure AD's object ID or UPN .

Get user Id from reference token in API

My setup,
An IdentityServer using MVC Identity to store the Users, created with dotnet new mvc -au Individual and applying the http://docs.identityserver.io/en/release/quickstarts/0_overview.html tutorial, running in localhost 5000.
A client App, but now I'm using postman to do tests.
A WEB API, created with dotnet new webapi, running in localhost 5001.
The IdentityServer resources and clients configuration is the following, notice that I'm using reference tokens:
public static IEnumerable<IdentityResource> GetIdentityResources() {
return new List<IdentityResource>{ new IdentityResources.OpenId() };
}
public static IEnumerable<ApiResource> GetApiResources() {
return new List<ApiResource>{
new ApiResource("api_resource", "API Resource") {
Description= "API Resource Access",
ApiSecrets= new List<Secret> { new Secret("apiSecret".Sha256()) },
}
};
}
public static IEnumerable<Client> GetClients() {
return new List<Client>{
new Client {
ClientId= "angular-client",
ClientSecrets= { new Secret("secret".Sha256()) },
AllowedGrantTypes= GrantTypes.ResourceOwnerPassword,
AllowOfflineAccess= true,
AccessTokenType = AccessTokenType.Reference,
AlwaysIncludeUserClaimsInIdToken= true,
AllowedScopes= { "api_resource" }
}
}
The password and user is send with postman and the token received is send to the WEB API also with postman, something like call localhost:5001/v1/test with the token pasted in option bearer token.
In the API Startup, in ConfigureServices I'm adding the lines below
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority= "http://localhost:5000";
options.ApiName= "api_resource";
options.ApiSecret = "apiSecret";
});
And I'm getting the Id of the user inside the controller as follows:
public async Task<IActionResult> Get(int id) {
var discoveryClient = new DiscoveryClient("http://localhost:5000");
var doc = await discoveryClient.GetAsync();
var introspectionClient = new IntrospectionClient(
doc.IntrospectionEndpoint,
"api_resource",
"apiSecret");
var token= await HttpContext.GetTokenAsync("access_token");
var response = await introspectionClient.SendAsync(
new IntrospectionRequest { Token = token });
var userId = response.Claims.Single(c => c.Type == "sub").Value;
}
The question itself is, am I using the right path to get the Id from the reference token?, because now It works but I don't want to miss anything, specially thinking that is a security concern.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference tokens.
Thanks in advance.
Inside a controller action that is protected with an [Authorize] attribute you can simply get claims directly from the ClaimsPrinciple, without having to go through a manual discovery client. The claims principle is handily aliased simply with User inside your controllers.
I'm asking also because I have seen anothers using
string userId = User.Claims.FirstOrDefault(c => c.Type ==
ClaimTypes.NameIdentifier).Value;
that is more straightforward but doesn't seems to fit with reference
tokens.
It works just fine with reference tokens. You should have no problems accessing the sub claim.
EDIT:
As I mentioned in a comment below, I tend to use the standard JwtClaimTypes and create some extension methods on the ClaimsPrinciple, such as:
public static string GetSub(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Subject))?.Value;
}
or
public static string GetEmail(this ClaimsPrincipal principal)
{
return principal?.FindFirst(x => x.Type.Equals(JwtClaimTypes.Email))?.Value;
}
... so that within my protected actions I can simply use User.GetEmail() to get hold of claim values.
It's worth stating the obvious, that any method for retrieving claim values will only work if the claims actually exist. i.e. asking for the ZoneInfo claim will not work unless that claim was requested as part of the token request in the first place.

Identity Server 4 - Logout - Passing Additional Data

When a user logs out under certain circumstances I want to show them a message on the logged out page. To enable this I want to be able to send an optional parameter from the client to the Identity Server / Authority site on logout.
While I have the standard logout flow working I have hit a brick wall in handling this scenario as information seems thin on the ground and the suggested solutions are not working.
From what I have read the 'state' parameter is the correct way to pass this information but this not coming through currently. AcrValues are only used to send information the other way.
My naive implementation below simply adds a state query string item to the end session endpoint. However, when I check the query string my client uses to go to the identity server instance it is missing.
Redirect(discoveryResponse.EndSessionEndpoint+"&state=foo")
Any help gladly received!
Current flow for MVC client:
Please note; some code has been removed for brevity.
Logout initiated from client controller with state=foo:
public class LogoutController : Controller
{
public ActionResult Index()
{
Request.GetOwinContext().Authentication.SignOut();
var discoveryClient = new DiscoveryClient(clientConfig.Authority) { Policy = {RequireHttps = false} };
var discoveryResponse = discoveryClient.GetAsync().Result;
var tokenClaim = ((ClaimsIdentity)User.Identity).FindFirst("id_token");
return Redirect(discoveryResponse.EndSessionEndpoint+ "?id_token_hint="+ tokenClaim + "&state=foo");
}
}
RedirectToIdentityProvider is called for request:
IdTokenHint and PostLogoutRedirectUri are set and passed correctly.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
return Task.FromResult(0);
var idTokenHint = n.OwinContext.Authentication.User.FindFirst(OpenIdConnectClaimType.IdToken);
if (idTokenHint == null) return Task.FromResult(0);
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
n.OwinContext.Response.Cookies.Append("IdentityServerPostLogoutReturnUri",
n.ProtocolMessage.PostLogoutRedirectUri);
n.ProtocolMessage.PostLogoutRedirectUri =
n.Options.PostLogoutRedirectUri;
return Task.FromResult(0);
}
}
URL Generated (not the lack of 'state' item):
http://localhost:44362/connect/endsession?post_logout_redirect_uri=http%3a%2f%2flocalhost%3a2577%2fpostloginredirect&id_token_hint=removed&x-client-SKU=ID_NET&x-client-ver=1.0.40306.1554
Logout page on the authority site:
This is where I want to be able to access the state parameter.
public class LogoutController : Controller
{
public async Task<ViewResult> Index(string logoutId)
{
if (logoutId == null) throw new Exception("Missing logoutId");
var logoutRequest = await interactionService.GetLogoutContextAsync(logoutId);
var vm = new LoggedOutViewModel(logoutRequest, logoutId);
if (!string.IsNullOrWhiteSpace(httpContextService.GetCookieValue(PostLogoutReturnUriCookieKey)))
{
vm.PostLogoutRedirectUri = httpContextService.GetCookieValue(PostLogoutReturnUriCookieKey);
httpContextService.ClearCookie(PostLogoutReturnUriCookieKey);
}
await httpContextService.SignOutAsync();
return View("Index", vm);
}
}
I've dug a little deeper and found what the issue was being caused by the following lines in the Microsoft.Owin.Security.OpenIdConnect middleware.
protected override async Task ApplyResponseGrantAsync()
{
AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode);
if (signout != null)
{
// snip
var notification = new RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options)
{
ProtocolMessage = openIdConnectMessage
};
await Options.Notifications.RedirectToIdentityProvider(notification);
// This was causing the issue
if (!notification.HandledResponse)
{
string redirectUri = notification.ProtocolMessage.CreateLogoutRequestUrl();
if (!Uri.IsWellFormedUriString(redirectUri, UriKind.Absolute))
{
_logger.WriteWarning("The logout redirect URI is malformed: " + redirectUri);
}
Response.Redirect(redirectUri);
}
}
}
In order to prevent the middleware from overriding the redirect when it detects a sign out message the following line in the 'HandleResponse' method needs to be called in the RedirectToIdentityProvider event.
This allows the original 'state' query string item to be passed to Identity Server and be pulled out using the interaction service.
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
// Snip
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// Snip
},
RedirectToIdentityProvider = n =>
{
// Snip
n.HandleResponse(); // The magic happens here
}
}

ASP.NET Identity and Claim-based

How to use claims? For example, I want to set access to each page (resource) for each user. I understand, I can do it using roles, but as I understand, claim-based is more effectively. But when I try to create a claim, I see the following method:
userIdentity.AddClaim(new Claim(ClaimTypes.Role, "test role"));
first parameter of constructor of Claim class get ClaimTypes enum, which has many "strange" members like Email, Phone etc. I want to set that this claim and then check this claim to have access to certain resource. I'm on wrong way? How to do it?
From the code above, I am assuming you have already added the claim in startup class on authenticated of your provider as below.
context.Identity.AddClaim(new Claim("urn:google:name", context.Identity.FindFirstValue(ClaimTypes.Name))); // added claim for reading google name
context.Identity.AddClaim(new Claim("urn:google:email", context.Identity.FindFirstValue(ClaimTypes.Email))); // and email too
Once you have added the claims in startup, when the request is actually processed check if its a callback and if yes, read the claims as below(in IHttpHandler).
public void ProcessRequest(HttpContext context)
{
IAuthenticationManager authManager = context.GetOwinContext().Authentication;
if (string.IsNullOrEmpty(context.Request.QueryString[CallBackKey]))
{
string providerName = context.Request.QueryString["provider"] ?? "Google";//I have multiple providers so checking if its google
RedirectToProvider(context, authManager, providerName);
}
else
{
ExternalLoginCallback(context, authManager);
}
}
If its 1st call redirect to provider
private static void RedirectToProvider(HttpContext context, IAuthenticationManager authManager, string providerName)
{
var loginProviders = authManager.GetExternalAuthenticationTypes();
var LoginProvider = loginProviders.Single(x => x.Caption == providerName);
var properties = new AuthenticationProperties()
{
RedirectUri = String.Format("{0}&{1}=true", context.Request.Url, CallBackKey)
};
//string[] authTypes = { LoginProvider.AuthenticationType, DefaultAuthenticationTypes.ExternalCookie };
authManager.Challenge(properties, LoginProvider.AuthenticationType);
//without this it redirect to forms login page
context.Response.SuppressFormsAuthenticationRedirect = true;
}
And finally read the claims you get back
public void ExternalLoginCallback(HttpContext context, IAuthenticationManager authManager)
{
var loginInfo = authManager.GetExternalLoginInfo();
if (loginInfo == null)
{
throw new System.Security.SecurityException("Failed to login");
}
var LoginProvider = loginInfo.Login.LoginProvider;
var ExternalLoginConfirmation = loginInfo.DefaultUserName;
var externalIdentity = authManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);
var emailClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email);
var email = emailClaim.Value;
var pictureClaim = externalIdentity.Result.Claims.FirstOrDefault(c => c.Type.Equals("picture"));
var pictureUrl = pictureClaim.Value;
LogInByEmail(context, email, LoginProvider); //redirects to my method of adding claimed user as logged in, you will use yours.
}
Claim doesn't set permission. It's used to verify you that "you are who you claim to be you are". These claims are identified by issuer, usually a 3rd party. See for example this article for description.
So, you should define which claims are necessary (who user should be) in order to access a certain page. Otherwise, using claim-based authorization will be same as using identity based or role based.

Oauth authentication with owin & Nancy

Following this guide for external auth using MVC 5 on Owin - External login providers with owinkatana.
I have added the following to my Owin Nancy application
Startup.cs -
app.Properties["Microsoft.Owin.Security.Constants.DefaultSignInAsAuthenticationType"] = "ExternalCookie";
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "ExternalCookie",
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Passive,
});
app.UseTwitterAuthentication(new TwitterAuthenticationOptions
{
ConsumerKey = "mykey",
ConsumerSecret = "mypass"
});
LoginModule.cs (nancy module)
Post["ExternalLogin"] = _ =>
{
var provider = Request.Form.name;
var auth = Context.GetAuthenticationManager();
auth.Challenge(new AuthenticationProperties
{
RedirectUri = String.Format("/?provder={0}", provider)
}, provider);
return HttpStatusCode.Unauthorized;
};
Now at the challenge point here nothing happens whatsoever. It just shows a blank page with the Url of the redirect. I have confirmed that I can get it to work following the example in MVC.
Does anyone know the correct Nancy code for this section?
I'll expand on a comment I was about to leave and just make it an answer (even though you moved away from Nancy it seems). I asked a similar question, and was pointed to the following code example on github:
https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/tree/dev/samples/Nancy/Nancy.Client
Assuming you have your OIDC wired up properly in Startup.cs, the following code is what I needed to get Nancy module to trigger the authentication on my signin/signout routes:
namespace Nancy.Client.Modules {
public class AuthenticationModule : NancyModule {
public AuthenticationModule() {
Get["/signin"] = parameters => {
var manager = Context.GetAuthenticationManager();
if (manager == null) {
throw new NotSupportedException("An OWIN authentication manager cannot be extracted from NancyContext");
}
var properties = new AuthenticationProperties {
RedirectUri = "/"
};
// Instruct the OIDC client middleware to redirect the user agent to the identity provider.
// Note: the authenticationType parameter must match the value configured in Startup.cs
manager.Challenge(properties, OpenIdConnectAuthenticationDefaults.AuthenticationType);
return HttpStatusCode.Unauthorized;
};
Get["/signout"] = Post["/signout"] = parameters => {
var manager = Context.GetAuthenticationManager();
if (manager == null) {
throw new NotSupportedException("An OWIN authentication manager cannot be extracted from NancyContext");
}
// Instruct the cookies middleware to delete the local cookie created when the user agent
// is redirected from the identity provider after a successful authorization flow.
manager.SignOut("ClientCookie");
// Instruct the OpenID Connect middleware to redirect
// the user agent to the identity provider to sign out.
manager.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType);
return HttpStatusCode.OK;
};
}
}
}
Code source: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/dev/samples/Nancy/Nancy.Client/Modules/AuthenticationModule.cs
Hope that helps!

Categories