I am trying to configure Identity 4 server to work with my API project. At this moment I can request token but I need to add user name and role to payload. I tried with IProfileService but no action was performed. How can I obtain this information from windows authentication? Here is my configuration:
launchSettings.json
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false
Program.cs
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseKestrel()
.UseIISIntegration()
.UseStartup<Startup>();
Startup.cs
services.Configure<IISOptions>(iis =>
{
iis.AutomaticAuthentication = true;
});
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(IdentityResourcesConfig.Get())
.AddInMemoryApiResources(ApiResourcesConfig.Get())
.AddInMemoryClients(ClientsConfig.Get());
ClientsConfig.cs
return new Client[]
{
new Client
{
ClientId = "XYC",
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "XYC" },
RequireClientSecret = false,
AlwaysIncludeUserClaimsInIdToken = true
}
};
I only worked with normal authentication but the classes that are creating and controll the way the claims are shared to other applications should be the same.
You probably just need to add the Claims to the API ressource because by default the claims used by the client will be not inlcuded into the Access Token also given to the client to request an API.
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[]
{
new ApiResource("MyApi", "This is my Api name", new List<string> {
"mynameclaimclaimname",
}),
the claim name you add in there is Name of claim.
If this is not working it would be helpful to give us further information. How are the API Ressources configured ( IdentityServer side and client side) ?
Or do you try to configure an API as Client ?
The first point is in IdentityServer, Windows authentication is an external provider (as opposed to the IS native authentication cookie). Windows authentication is triggered by using the ChallengeAsync API on the HttpContext using the scheme Windows.You can click
here for details .
Another point is you are using client credential flow , which is wrong in your scenario . Client credential flow use app's identity , there is no user in it .
Related
I'm using Azure Active Directory to provide authentication to the Backoffice on my website running Umbraco version 11.0.
This is working nicely and I can log in but I want to improve the experience by using app roles within Azure to manage the user's group within Umbraco.
My Azure setup
I've created an App Registration within Azure with the following configuration:
Added a Redirection URI:
URI: https://localhost:44391/umbraco-signin-microsoft/
Enabled Access tokens (used for implicit flows)
Enabled ID tokens (used for implicit and hybrid flows)
Supported account types: Accounts in this organizational directory only (Example only - Single tenant)
Added App Roles
Administrator
Editor
In Enterprise Applications, I've also added the App Roles above to my users:
My code
Login Provider
namespace Example.Api.Features.Authentication.Extensions;
public static class UmbracoBuilderExtensions
{
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
{
builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
const string schema = MicrosoftAccountDefaults.AuthenticationScheme;
logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddMicrosoftAccount(
// the scheme must be set with this method to work for the back office
backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName) ?? string.Empty,
options =>
{
//By default this is '/signin-microsoft' but it needs to be changed to this
options.CallbackPath = "/umbraco-signin-microsoft/";
//Obtained from the AZURE AD B2C WEB APP
options.ClientId = "CLIENT_ID";
//Obtained from the AZURE AD B2C WEB APP
options.ClientSecret = "CLIENT_SECRET";
options.TokenEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/token";
options.AuthorizationEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/authorize";
});
});
});
return builder;
}
}
Auto-linking accounts
namespace Example.Api.Features.Configuration;
public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
public const string SchemeName = "OpenIdConnect";
public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
{
if (name != "Umbraco." + SchemeName)
{
return;
}
Configure(options);
}
public void Configure(BackOfficeExternalLoginProviderOptions options)
{
options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
// must be true for auto-linking to be enabled
autoLinkExternalAccount: true,
// Optionally specify default user group, else
// assign in the OnAutoLinking callback
// (default is editor)
defaultUserGroups: new[] { Constants.Security.EditorGroupAlias },
// Optionally you can disable the ability to link/unlink
// manually from within the back office. Set this to false
// if you don't want the user to unlink from this external
// provider.
allowManualLinking: false
)
{
// Optional callback
OnAutoLinking = (autoLinkUser, loginInfo) =>
{
// You can customize the user before it's linked.
// i.e. Modify the user's groups based on the Claims returned
// in the externalLogin info
autoLinkUser.IsApproved = true;
},
OnExternalLogin = (user, loginInfo) =>
{
// You can customize the user before it's saved whenever they have
// logged in with the external provider.
// i.e. Sync the user's name based on the Claims returned
// in the externalLogin info
return true; //returns a boolean indicating if sign in should continue or not.
}
};
// Optionally you can disable the ability for users
// to login with a username/password. If this is set
// to true, it will disable username/password login
// even if there are other external login providers installed.
options.DenyLocalLogin = true;
// Optionally choose to automatically redirect to the
// external login provider so the user doesn't have
// to click the login button. This is
options.AutoRedirectLoginToExternalProvider = true;
}
}
In this file, I'd ideally do as the comment says and i.e. Modify the user's groups based on the Claims returned in the externalLogin info.
Also registered in my Startup file
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.ConfigureAuthentication()
.Build();
I've attempted to give the following permissions to the application, with no luck:
Current state of play is that I can login just fine but if I debug externalInfo, there's nothing in there about the users having either the Administrator or Editor App Role as configured above.
My gut feeling is that I'm missing something with the Azure Active Directory setup but I've tried a few different configurations and can't seem to get the App Roles to come back.
Thanks,
Ben
EDIT - 15.02.2023:
I can see that the roles come back when I hit the https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token endpoint using client_credentials as the grant_type. It looks like the .NET application using authorization_code instead. I've decoded the token retrieved from this and it doesn't contain the roles.
I wonder if there's some kind of configuration on the .NET application that allows me to add the roles.
To get App Roles in token claims, you can use client credentials flow to generate access tokens by granting admin consent.
I tried to reproduce the same in my environment via Postman and got the below results:
I registered one Azure AD web application and created App roles like below:
Now I assigned these App roles to users under it's Enterprise application like below:
Add these App roles in API permissions of application like below:
You can see App roles under Application permissions like below:
Make sure to grant admin consent to above permissions like this:
While generating access token, scope should be your Application ID URI ending with /.default
Now, I generated access token using client credentials flow via Postman with below parameters:
POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
client_id: <appID>
grant_type:client_credentials
scope: api://<appID>/.default
client_secret: secret
Response:
When I decoded the above token in jwt.ms, I got App roles in roles claim successfully like below:
Note that App roles are Application permissions that will work only with flows like client credentials which do not involve user interaction.
So, if you use delegated flows like authorization code flow, username password flow, etc..., you won't get App roles in token claims.
UPDATE:
You can use below c# code in getting access token from client credentials flow like this:
using Microsoft.Identity.Client;
var clientID = "bbb739ad-98a4-4566-8408-dxxxxxxxx3b";
var clientSecret = "K.k8Q~hwtxxxxxxxxxxxxxxxU";
var tenantID = "fb134080-e4d2-45f4-9562-xxxxxx";
var authority = $"https://login.microsoftonline.com/{tenantID}";
var clientApplication = ConfidentialClientApplicationBuilder.Create(clientID)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
var scopes = new string[] { "api://bbb739ad-98a4-4566-8408-xxxxxx/.default" };
var authenticationResult = await clientApplication.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
var accesstoken = authenticationResult.AccessToken;
Console.WriteLine(accesstoken);
Response:
When I decoded the above token, it has roles claim with App roles like below:
To solve this, I ended up swapping out the AddMicrosoftAccount AuthenticationBuilder in favour of AddOpenIdConnect. This appears to respect the claims in the tokens.
This is the code I am now using in the ConfigureAuthentication method.
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
{
// Register OpenIdConnectBackOfficeExternalLoginProviderOptions here rather than require it in startup
builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddOpenIdConnect(
// The scheme must be set with this method to work for the back office
backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName),
options =>
{
options.CallbackPath = "/umbraco-signin-microsoft/";
// use cookies
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// pass configured options along
options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
options.ClientId = "{clientId}";
options.ClientSecret = "{clientSecret}";
// Use the authorization code flow
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
// map claims
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.RequireHttpsMetadata = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UsePkce = true;
options.Scope.Add("email");
});
});
});
return builder;
}
We have 2 web applications on Angular (FE_1, FE_2) and 3 API applications on .NET Core (see picture)
Need to login at once time from one site and working between two without any additional authorization processes
I mean, when we log in to site1 and receive token #1, I want this token to work with API_2 as well, and vice versa, when we log in to site2 and receive token #2, I want to use this token to work as well with API_1
So my question is how to properly configure applications in Azure and configure them internally based on the described architecture ?
Thankyou Manish. Posting your suggestion as an answer to help the other community members.
from the provided architecture in BE code add the list of valid audiences as per the below image make it to true
Below is the sample code where you can check the valid audience code.
services.AddAuthentication(cfg =>
{
cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
opt.Authority = "https://login.microsoftonline.com/common";
opt.Audience = "api://A134d6c8-8078-2924-9e90-98cef862eb9a"; // Set this to the App ID URL for the web API, which you created when you registered the web API with Azure AD.
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidAudiences = new List<String>
{
// you could add a list of valid audiences
"A134d6c8-8078-2924-9e90-98cef862eb9a"
},
ValidIssuers = new List<string>
{
// Add tenant id after https://sts.windows.net/
"https://sts.windows.net/{YourTenantId}"
}
};
opt.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = AuthenticationFailed
};
});
For complete information check the SO.
Please help me to understand the difference between JWT token validation from the ASP netcore application and the netcore Kestrel hosted application.
There are two applications that verifies token using the source code like below:
public static IServiceCollection AddJwtToken(this IServiceCollection services, OAuthConfig config)
{
services.AddMvc();
services.AddAuthorization();
Logger.DebugFormat("AddJwtBearer authority:{0} audience:{1}", config.GetAuthority(), config.Resource);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => new JwtBearerOptions
{
Authority = config.GetAuthority(),
Audience = config.Resource,
});
return services;
}
it is pretty simple and it works well if token is being validated from the asp net core 2.2 application
// in the asp.net core
var builder = WebHost.CreateDefaultBuilder(args);
builder
.UseStartup<Startup>()
.ConfigureKestrel(_ => _.ConfigureEndpoints())
.UseSerilog();
And there is another application (console) that starts the same rest service host using the UseKestrel
//in the console app
var builder = WebHost.CreateDefaultBuilder()
.UseNLog()
.UseKestrel(_ => _.ConfigureEndpoints())
.UseStartup<Startup>();
the only one significant difference is that there is UseKestrel in the console via ConfigureKestrel for asp.net core.
The same source code (and configuration) is used to get token from the Azure AD.
Please find it as the gist here.
It is configured to get token from the https://login.microsoftonline.com/{tenant}/v2.0 provider. The same token endpoint, clientid, secret and scope values are used for both cases.
The problem is that AddJwtBearer validates the token in the asp.net core and does not in the console app.
the error is
Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10501: Signature validation failed. Unable to match keys:
kid: 'BB8CeFVqyaGrGNuehJIiL4dfjzw',
token: '{"typ":"JWT","alg":"RS256","kid":"BB8CeFVqyaGrGNuehJIiL4dfjzw"}.{"aud":"2c163c99-935b-4362-ae0d-657f589f5565","iss":"https://login.microsoftonline.com/{tenantidhere}/v2.0
Why asp.net core host validates the token (for the first AddJwtBearer implementation) and console host fails?
Thank you
to solve this error I've to load keys from the openid provider as below:
Logger.DebugFormat("AddJwtBearer authority:{0} audience:{1}", config.GetAuthority(), config.Resource);
IList<string> validissuers = new List<string>()
{
config.GetAuthority(),
};
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>($"{validissuers.Last()}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
var openidconfig = configManager.GetConfigurationAsync().Result;
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ =>
{
_.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateAudience = true,
ValidAudience = config.Resource,
ValidateIssuer = true,
ValidIssuers = new[] { config.GetAuthority() },
ValidateIssuerSigningKey = true,
IssuerSigningKeys = openidconfig.SigningKeys,
RequireExpirationTime = true,
ValidateLifetime = true,
RequireSignedTokens = true,
};
_.RequireHttpsMetadata = false;
});
And it started to work for both cases. But what is the difference with the old AddJwtBearer implementation and the new one (related to the keys validation)? Keys where downloaded and supplied using the IssuerSigningKeys = openidconfig.SigningKeys but why it is not loaded automatically using the .well-known/openid-configuration by the AddJwtBearer middleware ?
In my case, the same error was because of inadvertent use of the token received from one environment (https://dev/identity) and validated in another environment (i.e. http://local/identity).
Hope anyone can help me with the following highly frustrating problem.
I have .NET WebApi which I secured with jwt authentication. In setting up the OAuthAuthorizationServerOptions-object I set the property AllowInsecureHttp for my token end point to false. Running my API locally on IISExpress and testing it with Postman works like a charm. If I set the property to true and ask for a token on my end point it works, if I set it to false I get a 404 as expected. But when I publish the API to my production environment (Windows 2012/IIS8) and set the property to false, I can get a token over both https and http. It just doesn't seem to listen to the property.
I run the API as an application with alias Api under a domain with just one https-binding. I use the following helper base class for my API's:
public class BaseWebApi<TDerivedOAuthProvider> where TDerivedOAuthProvider : BaseOAuthProvider, new()
{
public BaseWebApi(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(DefaultAuthenticationTypes.ExternalBearer));
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
private static void ConfigureOAuth(IAppBuilder app)
{
var allowInsecureHttp = Convert.ToBoolean(ConfigurationManager.AppSettings["jwt::allowInsecureHttp"].ToString());
var issuer = ConfigurationManager.AppSettings["jwt::issuer"].ToString();
var secret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["jwt::secret"].ToString());
var clients = ConfigurationManager.AppSettings["jwt::clients"].ToString().Split(new char[] { ',' });
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = allowInsecureHttp,
AuthenticationType = DefaultAuthenticationTypes.ExternalBearer,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(10),
Provider = new TDerivedOAuthProvider(),
RefreshTokenProvider = new BaseRefreshTokenProvider(),
AccessTokenFormat = new BaseJwtFormat(issuer),
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ExternalBearer,
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = clients,
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
}
});
}
}
Variables allowInsecureHttp, issuer, secret and clients are filled from a configuration file. Setting the value for allowInsecureHttp hardcoded to false or true does not change anything. This base class is then instantiated by my actual API which (next to the actual API functions) also provides a CustomOAuthProvider class handling the actual implementation of the credential checking for this particular API:
[assembly: OwinStartup(typeof(MyCustomAPI.Startup))]
namespace MyCustomAPI.API
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
BaseWebApi<CustomOAuthProvider> webApi = new BaseWebApi<CustomOAuthProvider>(app);
}
}
}
Hope anyone can point me in the right direction. Set aside this issue the API itself is working flawlessly, but I really want to force SSL on my production tokens.
Cheers,
Nyquist
For anyone ever encountering the same issue. Figured it out eventually. Worked like a charm, but the HSTS policy on the domain I configured my virtual application redirected all http Postman traffic automatically to https. Installed the application on a simple domain without SSL and on that domain SSL was perfectly enforced. sigh :)
I am having ASP.Net webapi which is hosted on Azure. Actually I am having two different subscription
For Azure Active Directory - say its demo.onmicrosoft.com
For hosting webapi. - say its hosted on abc.azurewebsites.net
My api which is hosted on abc.azurewebsites.net is registered on demo.onmicrosoft.com. My client app is a service app which is authenticating against a user that resides in demo.onmicrosoft.com. Authentication is basic authentication by passing user credentials and recieveing AccessToken from Azure Active Directory. After recieving token from demo.onmicrosoft.com I am calling api from abc.azurewebsites.net. Like this:
https://abc.azurewebsites.net/api/some/queryapi
Now, In my controller if I use
[Authorize]
public class SomeController:ApiController
{
//my code
}
I am getting uauthorized access. and if I remove that attribute from controller it works fine.
Can you help me out.Even after app registration & authentication why I am getting so. Is that because of two different subscription or something else.
Update
I am sending token to my webapi this way
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
}
IMO, since my token is from azure ad of some other subscription so its not getting recognized by my webapi.
More Update - Startup.cs
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters {
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
},
});
}
Web.config
<add key="ida:Tenant" value="demo.onmicrosoft.com" />
<add key="ida:Audience" value="https://demo.onmicrosoft.com/4e4xxxx5-5xx1-4355-8xxc-705xxxx163" />
<add key="ida:ClientID" value="d0xxxxa-2xxx6-4xx-9e58-07xxxxxxxx1" />
It is relative to how you protect the web API instead of Azure subscription. For example, here is a piece of code which used to protect web API using Azure AD in the .net core:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Add the console logger.
loggerFactory.AddConsole(LogLevel.Debug);
// Configure the app to use Jwt Bearer Authentication
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]),
Audience = Configuration["AzureAd:Audience"],
Events = new JwtBearerEvents
{
OnTokenValidated = tokenValidated ,
OnAuthenticationFailed= AuthenticationFailed
}
});
}
This web API will verify the signature of the token to ensure the token is issued from Azure AD. Then it will check the aud claim in the access token. If the aud claim also matched as we config in above code the we can call the web API successfully.
1). Make sure Azure Authentication setting off.
2). In StartupAuth.cs
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
// Note: Remove the following line before you deploy to production:
AllowInsecureHttp = true
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
follow this article: https://www.asp.net/web-api/overview/security/individual-accounts-in-web-api