Add OneLogin as an OIDC to IdentityServer4 - 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.

Related

Azure B2C Logout in Blazor

I've build a Blazor server app and I'm using the Azure b2c which I build using the wizard.
I don't have a login page and I only use the Google as oauth provider. I just have the default blanket redirect which is fine for me.
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
One issue is that I'm facing is that when I'm opening the app it doesn't prompt me asking which account I want to use. I know I'm already signed into my google account as whole but when opening my app I would like the app to prompt for "choosing the account". When I run the same userflow on the azure portal it does prompt me. The same just doesn't happen for my app. How can I make sure that the app always asks to select the account? I read some articles which said to add "prompt" keyword but I don't know where to add that as I'm not calling any custom url.
Another issue I'm facing is that the log-out doesn't work as expected. In my app logout sequence is same as the default which redirects user to MicrosoftIdentity/Account/SignIn link.
<AuthorizeView>
<Authorized>
Hello, #context.User.Identity?.Name!
Log out
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</AuthorizeView>
Once I click the logout button; I does something and then redirects me to this page.
However once I click the back button, the app opens as normal with the user still signed in. I expected the app to prompt for login at-least this time.
Can you please help me with the right approach for the implementing this. I prefer to avoid advance things like custom user flows. Perhaps some settings in appsettings.json can do the trick?
Thanks a lot.
For the logout to work properly ,In the Redirect URIs section in portal, setredirect URIs.
Example:
redirectUri : https://localhost:44365/signin-oidc
In the Logout URL section, https://localhost:44365/signout-oidc or : https://localhost:44365/signin-oidc
Please check Configure session behavior - Azure Active Directory B2C | Microsoft Learn
In appsetting.json set "CallbackPath": "/signin-oidc" and set a userflow SignedOutCallbackPath
appsettings.json:
"AzureAd": {
"Authority": "https://xx.b2clogin.com/XXXXXX.onmicrosoft.com/B2C_1_SignUpSignIn",
"Instance": "https://XXXXXX.b2clogin.com",
"TenantId": "XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"ClientId": "XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"ClientSecret": "XXXXXXXXXXXXXXXXXX"
"CallbackPath": "/signin-oidc",
"Domain": "XXXXXX.onmicrosoft.com",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn",
"SignedOutCallbackPath": "/signout/B2C_1_susi",
"ResetPasswordPolicyId": "B2C_1_PasswordReset",
"EditProfilePolicyId": "B2C_1_EditProfile",
},
"API": {
"BaseUrl": "",
"Scopes": "https://XXXXXX.onmicrosoft.com/ xxx/<scope>"
},
In startup.cs:
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
Or
set the prompt value to login or select_account using OnRedirectToIdentityProvider when OIDC authentication handler is registered
public void ConfigureServices(IServiceCollection services)
{
.....
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, options =>
{
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
if (context.Properties.Items.TryGetValue("prompt", out string prompt))
{
context.ProtocolMessage.Prompt = prompt;
}
return Task.CompletedTask;
}
};
...
}
When I logged in
Selected logout which redirects to signout path which redirects to post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A44365%2Fsignout-callback-oidc&state=
Then if I clicked back , then I cant access other areas without login
Reference : https://github.com/Azure-Samples/ms-identity-blazor-server/blob/main/WebApp-OIDC/B2C/blazorserver-B2C/appsettings.json

IdentityServer4 - How to Logout Blazor Webassembly Client from IdentityServer

i am playing around with Blazor WASM and IdentityServer4. Login/Logut flows invoked from the client are all working well. Used Microsofts documentation found here Microsofts Docs
IdentityServer4 is hosted as a seperate Microservice as well as the Blazor WASM App - two indepented projects.
Now i am facing the problem of signing out from the IdentiyServer4. Invoking the logout from the IdentityServer4 UI doesnt logout the user from the Blazor WASM App. I already read this explenation signout IdentityServer4
"oidc": {
"Authority": "http://localhost:8010/",
"ClientId": "demoportal.blazor",
"DefaultScopes": [
"openid",
"profile"
],
"PostLogoutRedirectUri": "http://localhost:8070/authentication/logout-callback",
"RedirectUri": "http://localhost:8070/authentication/login-callback",
"ResponseType": "code"
}
I havenĀ“t found anything so far to achieve the goal. From my unterstanding it has to be used as oidc connect session managements not front or backend channel policy. But i cant find any useful docs on microsofts site.
After lots of reading ive found the answer.
Microsoft descripes the SPA difficulties right here: Microsoft Handle-Token-Request-Errors
These pointed me to implement on my base component something like this:
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var user = (await authenticationStateTask).User;
if (user.Identity.IsAuthenticated)
{
var tokenResult = await AccessTokenProvider.RequestAccessToken();
if(tokenResult.Status == AccessTokenResultStatus.RequiresRedirect)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
}
}
}
It works like a charm.
Btw dont forget to include the token when configuring HttpClient.
services.AddHttpClient<YOURSERVICEHERE>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[] { "URI here" },
scopes: new[] { "your scope here" });
return handler;
})
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>()

Unable to configure ASP.NET for Azure Access Token

I was hoping that someone might be able to shed some light on issues that I'm having with authentication. I've mostly used this guidance: https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-overview
I've got a react app that is successfully (I think...) retrieving access tokens for my API:
const account = msalInstance.getActiveAccount();
if (account) {
msalInstance.acquireTokenSilent({
...apiToken,
account: account
}).then((response) => {
setToken(response.accessToken);
});
}
My requests place the token in the authorization header:
Headers
My token looks looks like: Token
API Registration
My API returns 401 whenever I use the Authorize attribute. Because my client is retrieving a token that looks correct, I'm assuming the issue is on my API. This is what I have in my startup auth:
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(
new TokenValidationParameters
{
// Check if the audience is intended to be this application
ValidAudiences = new[] { [MY_API_CLIENT_ID (SAME AS AUDIENCE IN TOKEN)], [MY API REGISTRATION URI] },
// Change below to 'true' if you want this Web API to accept tokens issued to one Azure AD tenant only (single-tenant)
// Note that this is a simplification for the quickstart here. You should validate the issuer. For details,
// see https://github.com/Azure-Samples/active-directory-dotnet-native-aspnetcore
ValidateIssuer = false,
ValidateAudience = false,
ValidateTokenReplay = false,
ValidateIssuerSigningKey = false,
ValidateLifetime = false,
ValidateActor = false, //all false for testing
},
new OpenIdConnectSecurityKeyProvider("https://login.microsoftonline.com/[MY_TENANT_ID]/v2.0/.well-known/openid-configuration")
),
});
Tough silent error here. I noticed that none of the validation callbacks were being invoked. I was missing the package Microsoft.Owin.Host.SystemWeb

Sending Azure token to API back-end keeps returning 401 Unauthorzied error when OnTokenValidated is added to the API

Recently we have created a React front-end which communicates with our API back-end following this tutorial: https://itnext.io/a-memo-on-how-to-implement-azure-ad-authentication-using-react-and-net-core-2-0-3fe9bfdf9f36
Just as in the tutorial we have set-up the authentication in the front-end with the adal-react library. We added/registered the front-end in azure.
Next we created our API (.Net Core 2) and also registered this in the azure environment, the config is setup in the appsettings:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantDomain": "our_azure_environment.onmicrosoft.com",
"TenantId": "our_azure_environment.onmicrosoft.com",
"ClientId": "our_front-end_azure_id_1234"
}
In the API we also added the JWT middleware in the ConfigureServices as follow:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = Configuration["AzureAd:ClientId"];
options.Authority = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}";
});
When testing (calling an endpoint from the front-end) after logging in the front-end works, the data is being returned and the user is authenticated (api endpoint has the Authorize attribute), when not logged in the api endpoint returns 401 (as it should).
The problem is as follows:
When I add the following piece of code to the API ConfigureServices (which I want to use to do some additional stuff after authenticating) :
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
//Check if user has a oid claim
if (!context.Principal.HasClaim(c => c.Type == "oid"))
{
context.Fail($"The claim 'oid' is not present in the token.");
}
return Task.CompletedTask;
}
};
suddenly, the calls to the API endpoint return a 401 (Unauthorized) error when logged in.. Though, if I remove the OnTokenValidated part it works fine.
When reaching the OnTokenValidated, the token should already be validated / authenticated or am I wrong?
IntelliSense also says; Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
Did I forgot to add some setting? My feeling tells me that it is propably a wrong setup in azure itself but I have actually no clue.
The same token which is send from the front-end to the API is also being send to the graph API, when doing this, graph asks to give consent and after agreeing it works. With this in mind I believe I should add some permission to the API or something but I am not sure.
UPDATE
juunas pointed out in his comment below that I was using the wrong ClaimsPrincipal value this fixed the initial problem but now the following gave me the 401 error:
In my ConfigureServices (before the AddAuthentication part) I have added the following to manage / add users to my AspNetUsers table (in my azure database):
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<TRSContext>()
.AddDefaultTokenProviders();
When adding this code to the pipeline, I once more get the 401 error in the front-end. Any clue why this is?
UPDATE2
I found the solution for above (update). This was caused due to AddIdentity taken over the Authentication from JWT. This can be avoided by adding:
Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
to .AddAuthentication options:
services.AddAuthentication(Options =>
{
Options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
More information about the above can be found here:
https://github.com/aspnet/Identity/issues/1376
The error appears in the first case due to the fact that .NET ClaimsPrincipal objects translate the oid claim type to: http://schemas.microsoft.com/identity/claims/objectidentifier.
So it needs to be like:
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
//Check if user has a oid claim
if (!context.Principal.HasClaim(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier"))
{
context.Fail($"The claim 'oid' is not present in the token.");
}
return Task.CompletedTask;
}
};

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