Azure Active Directory with IdentityServer4 (Microsoft.AspNetCore.Identity.UI) - Step by Step Guide? - identityserver4

I have a .NET Core app which uses identityserver4 to authenticate users. I have integrated it with ASP.NET Identity (Microsoft.AspNetCore.Identity.UI) and this works fine. It uses the AspNetUser tables etc. to store users. etc etc and all the options work.
I would like to add the option to use Azure Active Directory users. So I add the following code to my startup class (previously there was just services.AddAuthentication();):
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.Authority = "https://login.windows.net/<My Azure Tenant Guid>";
options.TokenValidationParameters =
new TokenValidationParameters { ValidateIssuer = true };
options.ClientId = "<My Azure App Client Id>";
options.CallbackPath = "/signin-aad";
options.SignedOutCallbackPath = "/signout-callback-aad";
options.RemoteSignOutPath = "/signout-aad";
options.ResponseType = OpenIdConnectResponseType.Code;
options.ClientSecret = "<My Azure App Client Secret>";
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.RequireHttpsMetadata = true;
})
;
This makes a button available to add your Azure AD account... Which doesn't work - it gets as far as asking for permission, then comes up with "Unexpected error occurred loading external login info".
Any ideas, or does anyone have a link to a good tutorial?

Related

Cannot retrieve data from Active Directory

I created a new Blazor application with Azure AD authentication. Login functionality works as expected. Now I need to retrieve a list of users of a particular group. On Azure AD, I created a Client Secret. Also, I added Microsoft.Graph permissions Directory.Read.All, Group.Read.All, and
GroupMember.Read.All, and granted admin consent for the permissions.
Here are the permissions:
Here is my code I use to retrieve the users of a group:
var scopes = new string[] { "https://graph.microsoft.com/.default" };
var confidentialClient = ConfidentialClientApplicationBuilder
.Create(_adSettings.ClientId)
.WithAuthority($"{_adSettings.Instance}/{_adSettings.TenantId}/ v2.0")
.WithClientSecret(_adSettings.ClientSecret)
.Build();
GraphServiceClient graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider(async (requestMessage) => {
// Retrieve an access token for Microsoft Graph (gets a fresh token if needed).
var authResult = await confidentialClient.AcquireTokenForClient(scopes).ExecuteAsync();
// Add the access token in the Authorization header of the API
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
}));
var members = await graphServiceClient.Groups["mygroup#mydomain.com"]
.Members
.Request()
.GetAsync();
The last statement throws an exception:
An error occurred sending the request. HttpStatusCode: 404: NotFound
I tried to replace it with
var users = await graphServiceClient.Users.Request().GetAsync();
But result was the same.
You should specify your group object ID instead of the group name:
My test group:
Test code and result:
UPDATE:
This is a C# console app code for this test, hope it helps :
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
using System;
namespace graphsdktest
{
class Program
{
static void Main(string[] args)
{
var clientId = "<Azure AD App ID>";
var clientSecret = "<App secret>";
var tenantID = "<tenant ID>";
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantID)
.WithClientSecret(clientSecret)
.Build();
ClientCredentialProvider authenticationProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authenticationProvider);
var result = graphClient.Groups["<group ID>"].Members.Request().GetAsync().GetAwaiter().GetResult();
foreach (var user in result) {
Console.WriteLine(user.Id);
}
}
}
}
UPDATE 2:
If you get some permission exception while you query members of a group, pls go to Azure AD => App registrations => find your app => API permissions => Add a permission => Microsoft graph api => application permission => GroupMember.Read.All :
And click This button to finish the grant process :

Add OneLogin as an OIDC to IdentityServer4

I am currently setting up IdentityServer4 with ASP.NET Core Identity, and I am trying to integrate this with OneLogin OIDC.
I have my IdentityServer4 service setup and running. I have added the Google scheme to this, so on my IdentityServer login page I have a login form and the Google login button.
I have created several client applications, an MVC app, a basic javascript app and also an Angular app.
With these clients I am able to authenticate against IdentityServer and get an access token, and then access a .NET Core WebAPI I have setup as an API scope.
My company uses OneLogin as our SSO, so I am trying to see if I can link IdentityServer to OneLogin.
In my IdentityServer Startup.cs ConfigureService method I have added the following
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "clientid";
options.ClientSecret = "secret";
})
.AddOpenIdConnect("oidc", "OneLogin", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.SaveTokens = true;
options.Authority = "https://companyname.onelogin.com/oidc/2";
options.ClientId = "clientid";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
I am able to view the following Provider Configuration from OneLogin for my corporate domain:
{
"acr_values_supported": ["onelogin:nist:level:1:re-auth"],
"authorization_endpoint": "https://companyname.onelogin.com/oidc/2/auth",
"claims_parameter_supported": true,
"claims_supported": ["sub", "email", "preferred_username", "name", "updated_at", "given_name", "family_name", "locale", "groups", "params", "phone_number", "acr", "sid", "auth_time", "iss"],
"grant_types_supported": ["authorization_code", "implicit", "refresh_token", "client_credentials", "password"],
"id_token_signing_alg_values_supported": ["HS256", "RS256", "PS256"],
"issuer": "https://companyname.onelogin.com/oidc/2",
"jwks_uri": "https://companyname.onelogin.com/oidc/2/certs",
"request_parameter_supported": false,
"request_uri_parameter_supported": false,
"response_modes_supported": ["form_post", "fragment", "query"],
"response_types_supported": ["code", "id_token token", "id_token"],
"scopes_supported": ["openid", "name", "profile", "groups", "email", "params", "phone"],
"subject_types_supported": ["public"],
"token_endpoint": "https://companyname.onelogin.com/oidc/2/token",
"token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"userinfo_endpoint": "https://companyname.onelogin.com/oidc/2/me",
"userinfo_signing_alg_values_supported": ["HS256", "RS256", "PS256"],
"code_challenge_methods_supported": ["S256"],
"introspection_endpoint": "https://companyname.onelogin.com/oidc/2/token/introspection",
"introspection_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"revocation_endpoint": "https://companyname.onelogin.com/oidc/2/token/revocation",
"revocation_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "none"],
"claim_types_supported": ["normal"]
}
I have a OneLogin developer account, and in there I have created a "OpenId Connect (OIDC)" application. Here I have the options to configure a Login URL and a Redirect URL.
I put the redirect URL as https://localhost:44361/signin-oidc, where localhost:44361 is my IdentityServer instance. I Initially put localhost:4200/login as the login URL, which is the URL of my angular application.
When I navigate to my Angular app, I am directed to my IdentityServer login page as expected. Here I have a button for "One Login". I click this button, which does then take me to OneLogin, again as expected. I enter my login credentials. OneLogin then redirects me to https://localhost:44361/signin-oidc. However, I receive the following error message:
An unhandled exception occurred while processing the request.
OpenIdConnectProtocolException: Message contains error: 'invalid_client', error_description: 'client authentication failed', error_uri: 'error_uri is null'.
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest)
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler<TOptions>.HandleRequestAsync()
Can anyone help me understand what I am missing? The OneLogin documentation doesn't seem to be very clear (no on screen help or tips when setting up the OIDC app).
I can't find any tutorials or documentation on IdentityServer4 and OneLogin, so I am wondering if what I want to achieve is even possible?
Sods Law.
I managed to find a solution in this OneLogin blog post, 20 minutes after I posted my question.
https://www.onelogin.com/blog/how-to-use-openid-connect-authentication-with-dotnet-core
The step I was missing was creating a Custom Connector before creating my OIDC Application in OneLogin.
With that connector in place, I am able to authenticate and I am returned to my Angular SPA.

Identity Server 4 with Azure AD - "We couldn't sign you in. Please try again."

I'm using .NET Core 3.1 with Identity Server 4 and connecting to Azure AD via OpenIdConnect. I'm using a Vue.js front-end and .NET Core API. IdentityServer, the front-end, and the API are all hosted on-prem on the same server (same domain). Everything uses https. I'm using an Oracle database with EF model first, with fully-customized IdentityServer stores and a custom user store (I implemented the interfaces). I'm using IdentityServer's Quickstart, edited a little to hook up my custom user store instead of the test user. I'm running this in my dev environment.
If I type in the url to the IdentityServer, I'm redirected to Azure AD, signed-in successfully, and shown this page:
Grants - successful login
The claims are coming back from Azure AD and the auto-provisioning is successful. It is written successfully to the database.
Authenticating through my JS client hits IdentityServer, redirects to Azure AD, I sign-in, then it redirects to IdentityServer's ExternalController, then redirects back to a Microsoft url, then proceeds to repeat until it finally fails with this page:
Sign-in failure from Azure AD
My guess is I messed up a redirect uri somewhere. Here is my code and the IdentityServer log:
IdentityServer Log
That block of logging repeats 6-10 times. No errors or anything different at the end.
I had to break up the C# code because the site couldn't handle one of my long options lines.
IdentityServer Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10),
CookieSlidingExpiration = true
};
}).AddClientStore<ClientStore>()
.AddCorsPolicyService<CorsPolicyService>()
.AddResourceStore<ResourceStore>()
.AddPersistedGrantStore<PersistedGrantStore>()
.AddProfileService<UserProfileService>();
services.AddScoped<IUserStore, UserStore>();
if (env.IsDevelopment())
{
// not recommended for production
builder.AddDeveloperSigningCredential();
}
else
{
// TODO: Load Signing Credentials for Production.
}
services.AddAuthentication()
.AddOpenIdConnect("aad", "Azure AD", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.windows.net/[authority]";
options.CallbackPath = "/callback-aad";
options.ClientId = "[ClientId]";
options.RemoteSignOutPath = "/signout-aad";
options.RequireHttpsMetadata = true;
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.SaveTokens = true;
options.SignedOutCallbackPath = "/signout-callback-aad";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
options.UsePkce = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
Client OIDC config:
const oidcSettings = {
authority: '[IdentityServerUrl]',
client_id: '[ClientId]',
post_logout_redirect_uri: '[front-end url]/logout-aad',
redirect_uri: '[front-end url]/callback-aad',
response_type: 'code',
save_tokens: true,
scope: 'openid profile',
}
Callback method being hit for ExternalController:
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
if (_logger.IsEnabled(LogLevel.Debug))
{
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
_logger.LogDebug("External claims: {#claims}", externalClaims);
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await 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 = await AutoProvisionUser(provider, providerUserId, claims);
}
// this allows us to collect any additional claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var isuser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.Username,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Azure AD config:
redirect uri: [IdentityServer url]/callback-aad
Database table data:
Client table IMG1
Client table IMG2
ClientScopes table
ClientRedirectUris table
Please let me know if you need any additional information. Thank you
The problem was in my custom UserStore. I was getting the user by the Azure AD SubjectId instead of the UserSubjectId. So in the ExternalController, the ApplicationUser object was coming up as null. Instead of an exception, it kept going back to Azure AD to try to get the user again, but obviously that just creates an infinite loop. I didn't think to look there since my user was successfully provisioned with Id's and claims.

Does IdentityServer4.AccessTokenValidation support validating tokens from multiple authorities?

Does IdentityServer4.AccessTokenValidation support authentication of multiple authorities?
Normally, I setup the trust for my own single authority like so:
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "TestApi";
});
I need to auth tokens from my own authority as well as one other trusted external authority. Not sure how to do it.

Azure B2C Persistent Cookie

I am using Azure B2C with one identity provider configured (LinkedIn). I have a Web API (b2c bearer auth) and a Web App MVC (b2c Open Id).
I'm trying to create a persistent login - meaning the user can login via LinkedIn once every 90 days from the given device+browser.
The closest I've gotten is when I added IsPersistent = true code in the web app to enable that:
Update: Updated code based on Azure B2C GA. To achieve where I was at with Preview, I still use a custom authorize attribute, but the code was updated:
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties()
{
IsPersistent = true
});
base.HandleUnauthorizedRequest(filterContext);
}
However, this is only valid for about 1 hour. Perhaps it is following the Access & ID policy? With no bounds on the refresh token - I am not sure why only 1 hour for "IsPersistent".
Token Session Config in Azure
So that leads to these questions:
Is a Persistent session (60-90 days) something I can achieve with Azure B2C (OpenId Connect)?
If so, any pointers on what I am missing? Do I need to do some custom cookie validation? Something with refresh tokens (I use them for the web api, but nothing custom in the web app).
Any thoughts or input would be great!
I have been able to achieve a persistent session with B2C after doing the following:
Custom Authorization Attribute
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.HttpContext.GetOwinContext()
.Authentication.Challenge(
new AuthenticationProperties() { IsPersistent = true }
);
base.HandleUnauthorizedRequest(filterContext);
}
Use Microsoft.Experimental.IdentityModel.Clients.ActiveDirectory instead of BootstrapContext (basically went with the pre-GA code sample (view change history) -> https://github.com/AzureADQuickStarts/B2C-WebApp-WebAPI-OpenIDConnect-DotNet). The ADAL library handles the getting a proper token transparent to my code.
Implemented custom TokenCache (based the EFADAL example here: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-multitenant-openidconnect/blob/master/TodoListWebApp/DAL/EFADALTokenCache.cs)
Changed Startup.Auth.cs:
return new OpenIdConnectAuthenticationOptions
{
MetadataAddress = String.Format(aadInstance, tenant, policy),
AuthenticationType = policy,
UseTokenLifetime = false,
ClientId = clientId,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
},
Scope = "openid offline_access",
ResponseType = "code id_token",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
SaveSigninToken = true,
},
}

Resources