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
Related
I am attempting to create a subscription to the resource /communications/onlineMeetings/?$filter=JoinWebUrl eq '{JoinWebUrl}' with the node ms graph client.
To do this, I have:
Two tenants, one with an active MS Teams license (Office 365 developer), whereas the other tenant houses my client app, which is a multi-tenant app.
Added the required scope to the client app (App level scope: OnlineMeetings.Read.All)
Given admin consent to the client app from the MS Teams tenant. The screenshot below shows the client app scope details in the MS Teams tenant.
Initialized the MSAL auth library as follows in the client app:
const authApp = new ConfidentialClientApplication({
auth: {
clientId: 'app-client-id',
clientSecret: 'app-client-secret',
authority: `https://login.microsoftonline.com/${tenantId}`,
},
});
Gotten an accessToken via the call:
const authContext = await authApp.acquireTokenByClientCredential({
authority: `https://login.microsoftonline.com/${tenantId}`,
scopes: ['https://graph.microsoft.com/.default'],
skipCache: true,
});
const accessToken = authContext.accessToken;
Initialized the MS Graph client as follows:
const client = MSClient.init({
debugLogging: true,
authProvider: (done) => {
done(null, accessToken);
},
});
Created a subscription successfully for the: CallRecords.Read.All scope (which correctly sends call record notifications to the defined webhook) with the following call:
const subscription = await client
.api('/subscriptions')
.version('beta')
.create({
changeType: 'created,updated',
notificationUrl: `https://my-ngrok-url`,
resource: '/communications/callrecords',
clientState: 'some-state',
expirationDateTime: 'date-time',
});
Attempted to create a subscription for the OnlineMeetings.Read.All scope with the following call:
const subscription = await client
.api('/subscriptions')
.version('beta')
.create({
resource: `/communications/onlineMeetings/?$filter=JoinWebUrl eq '{JoinWebUrl}'`,
changeType: 'created,updated',
notificationUrl: `https://my-ngrok-url`,
clientState: 'some-state',
expirationDateTime: 'date-time',
includeResourceData: true,
encryptionCertificate: 'serialized-cert',
encryptionCertificateId: 'cert-id',
});
This results in the error message:
GraphError: Operation: Create; Exception: [Status Code: Forbidden;
Reason: The meeting tenant does not match the token tenant.]
I am unsure what is causing this and and how to debug it further. Any help would be much appreciated.
My understanding of the joinWebUrl in the resource definition was incorrect -- the joinWebUrl is a placeholder for an actual online-meeting's URL (rather than a catch-all for all created meetings).
What I was hoping to achieve however is (currently) not possible, which is creating a subscription on a particular user's join/leave events to any online-meeting.
I am trying to configure my react/.NET 5.0 application to work with Azure B2C. I have everything set up , I have tried to run this against an MVC application and I get the login screen. But for some reason, when I try to redirect from a react page, I keep getting the same error. There appears to be almost no real good documentation for this as well. This is my authConfig file.
export const msalConfig = {
auth: {
clientId: process.env.REACT_APP_ADB2C_CLIENT_ID
, authority: process.env.REACT_APP_ADB2C_AUTHORITY
, knownAuthorities: [process.env.REACT_APP_KNOWN_AUTH]
, clientSecret: process.env.REACT_APP_CLIENT_SECRET
, reponseType: 'code'
},
cache: {
cacheLocation: 'sessionStorage'
,storeAuthStateInCoolie: false
}
};
const b2cPolicies = {
name: {
signUpSignIn: "B2C_1_cp_signin_up"
, forgotPassword: "B2C_1_cp_forgot_pwd"
, editProfile: "B2C_1_cp_edit_profile"
},
authorities: {
signUpSignIn: {
authority: `https://${process.env.REACT_APP_TENANT_LOGIN}/${process.env.REACT_APP_TENANT}/${process.env.REACT_APP_SIGNUP_POLICY}`,
},
forgotPassword: {
authority: `https://${process.env.REACT_APP_TENANT_LOGIN}/${process.env.REACT_APP_TENANT}/${process.env.REACT_APP_FORGOT_POLICY}`,
},
editProfile: {
authority: `https://${process.env.REACT_APP_TENANT_LOGIN}/${process.env.REACT_APP_TENANT}/${process.env.REACT_APP_EDIT_POLICY}`
}
},
authorityDomain: process.env.REACT_APP_TENANT_LOGIN
}
export const loginRequest = {
scopes: ["openid", "profile"],
};
I keep running into this error when I click on the link to redirect.
Any help with this would be great.
The reply URL must begin with the scheme https. Please check if reply urls are configured correctly which must be same in azure portal and in code .
Check if the callback path is set to identity provider something like /signin-oidc for redirect url .(And make sure you have unique callback if multiple urls are used.
such as https://contoso.com/signin-oidc.
The CallbackPath is the path where server will redirect during authentication. It's automatically handled by the OIDC middleware itself, that means we can't control the logic by creating a new controller/action and set CallbackPath to it
If you have check marked id_token in portal, try redirecting to home page ,instead of api actions directly.
Change the cookies to be set as secure using this in start up class
services.Configure(options =>
{
options.CheckConsentNeeded = context => true;//add if consent needed
options.MinimumSameSitePolicy = SameSiteMode.None; // else try SameSiteMode.Lax;
options.Secure = CookieSecurePolicy.Always;
});
use Microsoft.AspNetCore.HttpOverrides; reference in startup.cs class, by including the nuget package.
Also check and Add > app.UseHttpsRedirection(); above app.authentication(); in startup configure method.
then try again.
If not try to set ProtocolMessage.RedirectUri to the HTTPS url
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAdB2C", options);
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctx =>
{
/* Redirect Uri modified for https protocol */
ctx.ProtocolMessage.RedirectUri = urlWithHttps
}
}
});
Or you can pass login hint :Please refer this doc.
References:
Tutorial: Register an application - Azure AD B2C | Microsoft Docs
Configure authentication in a sample web application by using Azure Active Directory B2C | Microsoft Docs
I have an Identity Server 4 instance running at https://localhost:5443/ and a client React.js application running at http://localhost:3000/ and making a reference to the oidc-client library in order to establish the communication. I've been following more or less this article.
The way I've configured the client (in-memory) on the Identity Server is as follows:
new Client
{
ClientId = "react-js-implicit-flow",
ClientName = "Some App name",
ClientUri = "http://localhost:3000",
AllowedGrantTypes = GrantTypes.Implicit,
RequireClientSecret = false,
RedirectUris = { "http://localhost:3000/signin-oidc", },
PostLogoutRedirectUris = { "http://localhost:3000/signout-oidc" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"weatherapi.read"
},
AllowAccessTokensViaBrowser = true
}
and the way it looks like on the Ract.js app is like this:
In general, everything goes well. I can login and logout from the Identity Server but the issue is that here:
I get no value (it is null) and this stops the Identity server from redirecting me back to the client application right after logout. If I hard code it (http://localhost:3000/signout-oidc) it works. But for some reason it is just not available.
During the logout this is what the Identity Server logs show:
So, no error, no nothing but I still can not navigate back to the client app after logout.
You do not provide an idTokenHint (id token) with your logout request like the following:
const mgr = new Oidc.UserManager(settings);
const signoutUrl = await mgr.createSignoutRequest({id_token_hint: user.id_token});
window.location.href = signOutUrl.url;
//or
await mgr.signoutRedirect({id_token_hint: user.id_token});
//or just
await mgr.signoutRedirect();
//it will try to attach id_token internally
Lack of the token is the reason for Identityserver to skip the post_logout_redirect_url parameter.
Identityserver has to validate the parameter against the client's configuration, but without the token it can't.
What solved the issue for (me thanks to answer by #d_f) was to change something on the client side and more specifically: src/services/userService/signoutRedirect.
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;
}
};
I'm using Asp.net core rc2 with OpenIdConnectServer. I'm using angular 1.x with augular-oauth2. After a few days, my error has digressed to
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:54275/api/Account/Username
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: Successfully validated the token.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: HttpContext.User merged via AutomaticAuthentication from authenticationScheme: Bearer.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was successfully authenticated.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was forbidden.
My ConfigureServices consists of
services.AddAuthorization(options =>
{
options.AddPolicy("UsersOnly", policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("role");
});
});
My configure has
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
RequireHttpsMetadata = false,
Audience = "http://localhost:54275/",
Authority = "http://localhost:54275/",
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = "client1",
//ValidAudiences = new List<string> { "", "empty", "null"}
}
});
});
app.UseOpenIdConnectServer(options =>
{
options.AuthenticationScheme = OpenIdConnectServerDefaults.AuthenticationScheme;
options.Provider = new SimpleAuthorizationServerProvider();
options.AccessTokenHandler = new JwtSecurityTokenHandler();
options.ApplicationCanDisplayErrors = true;
options.AllowInsecureHttp = true;
options.TokenEndpointPath = new PathString("/oauth2/token");
options.LogoutEndpointPath = new PathString("/oauth2/logout");
options.RevocationEndpointPath = new PathString("/oauth2/revoke");
options.UseJwtTokens();
//options.AccessTokenLifetime = TimeSpan.FromHours(1);
});
My authorize attribute is defined on the Controller as
[Authorize(Policy = "UsersOnly", ActiveAuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme), Route("api/Account")]
I store the token as a cookie and attach it to requests using an http interceptor in angular.
I generate the token with
public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
{
// validate user credentials (demo mode)
// should be stored securely (salted, hashed, iterated)
using (var con = new SqlConnection(ConnectionManager.GetDefaultConnectionString()))
{
if (!Hashing.ValidatePassword(context.Password, await con.ExecuteScalarAsync<string>("SELECT PassHash FROM dbo.Users WHERE Username = #UserName", new { context.UserName })))
{
context.Reject(
error: "bad_userpass",
description: "UserName/Password combination was invalid."
);
return;
}
// create identity
var id = new ClaimsIdentity(context.Options.AuthenticationScheme);
id.AddClaim(new Claim("sub", context.UserName));
id.AddClaim(new Claim("role", "user"));
// create metadata to pass on to refresh token provider
var props = new AuthenticationProperties(new Dictionary<string, string>
{
{"as:client_id", context.ClientId}
});
var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), props,
context.Options.AuthenticationScheme);
ticket.SetAudiences("client1");
//ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.Email, OpenIdConnectConstants.Scopes.Profile, "api-resource-controller");
context.Validate(ticket);
}
}
I've spent the last three days on this problem and I realize that at this point I'm probably missing something obvious due to lack of sleep. Any help would be appreciated.
The error you're seeing is likely caused by 2 factors:
You're not attaching an explicit destination to your custom role claim so it will never be serialized in the access token. You can find more information about this security feature on this other SO post.
policy.RequireClaim("role"); might not work OTB, as IdentityModel uses an internal mapping that converts well-known JWT claims to their ClaimTypes equivalent: here, role will be likely replaced by http://schemas.microsoft.com/ws/2008/06/identity/claims/role (ClaimTypes.Role). I'd recommend using policy.RequireRole("user") instead.
It's also worth noting that manually storing the client_id is not necessary as it's already done for you by the OpenID Connect server middleware.
You can retrieve it using ticket.GetPresenters(), that returns the list of authorized presenters (here, the client identifier). Note that it also automatically ensures a refresh token issued to a client A can't be used by a client B, so you don't have to do this check in your own code.