I'm using Identity Server3 for my project, I currently have a website and api being protected by the Id server, this is working fine however because I'm storing the users in the Id Server database I can't really change any user's data from the website like changing the profile picture or any claim value.
In order to solve this I'm thinking in creating an API on top of IdServer, this API will manage the users, changing a password, retrieving users or changing anything related to a user basically, I want to create this API on the sample project where I have my IdServer using Owin mapping.
Right now I have my idServer in the /identity route like this
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Trace().CreateLogger();
app.Map("/identity", idserverApp =>
{
var efConfig = new EntityFrameworkServiceOptions
{
ConnectionString = "IdSvr3Config"
};
var options = new IdentityServerOptions
{
SiteName = "Identity server",
IssuerUri = ConfigurationManager.AppSettings["idserver:stsIssuerUri"],
PublicOrigin = ConfigurationManager.AppSettings["idserver:stsOrigen"],
SigningCertificate = Certificate.Get(),
Factory = Factory.Configure(efConfig),
AuthenticationOptions = AuthOptions.Configure(app),
CspOptions = new CspOptions { Enabled = false },
EnableWelcomePage=false
};
new TokenCleanup(efConfig, 3600 * 6).Start();
idserverApp.UseIdentityServer(options);
});
app.UseIdServerApi();
}
}
My api "middleware" is as this
**public static class IdServerApiExtensions
{
public static void UseIdServerApi(this IAppBuilder app, IdServerApiOptions options = null)
{
if (options == null)
options = new IdServerApiOptions();
if (options.RequireAuthentication)
{
var dic = app.Properties;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44302/identity", //here is the problem, it says not found even though I already mapped idServer to /identity
RequiredScopes = new[]
{
"idserver-api"
},
ValidationMode=ValidationMode.ValidationEndpoint
});
}
var config = new HttpConfiguration();
WebApiConfig.Register(config,options.RequireAuthentication);
app.UseNinjectMiddleware(() => NinjectConfig.CreateKernel.Value);
app.UseNinjectWebApi(config);
app.Use<IdServerApiMiddleware>(options);
}
}**
I know is possible I just don't know if it is a good idea to have the api on the same project or not and also I don't know how to do it.
Is it a good idea to create this API to manage the users? If not what could I use?
How can I create the proper settings to fire up the API under /api and at the same time use IdServer to protect this api?
Update
Adding ValidationMode=ValidationMode.ValidationEndpoint, fixed my problem, thanks to Scott Brady
Thanks
The general advice from the Identity Server team is to run any admin pages or API as a separate project (Most recent example). Best practice would be only to give your Identity Server and identity management applications access to your identity database/store.
To manage your users, yes, you could write your own API. Other options would be to contain it to a single MVC website or to use something like Identity Manager.
You can still use the same application approach however, using the OWIN map. To secure this you could use the IdentityServer3.AccessTokenValidation package, using code such as:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["idserver:stsOrigen"],
RequiredScopes = new[] { "adminApi" },
ValidationMode = ValidationMode.ValidationEndpoint
});
Related
I am currently working on a project where I need to work with Azure AD token, which is working fine. I register my application in Azure portal and use the client id in my project and this is working fine with this code (in startup.cs):
services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd");
and these settings in appsettings.json:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "35234523452345",
"ClientId": "25234523452345",
"Audience": "api://25234523452345"
},
I want to token only for one API call but with the above code it will apply on all the API. Can I use a custom filter which will use the Azure token for authentication?
Can someone help me with a code sample?
i found solution for my question, i created a method which will validate azure token, and will return me user name from user claim
public static async Task<string> ValidateAzureToken(string token, AppConfigurationList appConfigurations)
{
var tenantId = "Azure Tenant Id here";
var audience = string.Format("api://{0}", "Azure Client Id here");
var azureClientKey = "Azure Client secret Key here";
var myIssuer = string.Format(CultureInfo.InvariantCulture, "https://sts.windows.net/{0}/", tenantId);
var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(azureClientKey));
var stsDiscoveryEndpoint = string.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", tenantId);
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = await configManager.GetConfigurationAsync();
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = audience,
ValidateIssuer = true,
ValidIssuer = myIssuer,
IssuerSigningKeys = config.SigningKeys,
ValidateLifetime = false,
IssuerSigningKey = securityKey,
};
_ = (SecurityToken)new JwtSecurityToken();
SecurityToken validatedToken;
try
{
var claims = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return claims.Identity.Name.Split("#")[0];
}
catch (Exception e)
{
throw new TGUnauthorizedException("unauthorized domain user", e);
}
}
An ASP.NET Core web application that authenticates Azure AD users and calls a web API using OAuth 2.0 access tokens.
Here you can find the ASP.NET Core samples about JwtBearer. Here the Azure AD samples demonstrate WebApp calling WebApi (also bearer for the WebAPI, OIDC for the App FrontEnd.
Sample Code: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore
Reference SO Thread: Azure AD Authentication in ASP.NET Core 2.2
I think you can follow this document to set authentication for the specific api.
Based on the configuration you've done, you can manage your api controller like this
[Authorize]
public class HelloController : Controller
{
public IActionResult Index()
{
HttpContext.ValidateAppRole("Tiny.Read");
Student stu = new Student();
stu.age = 18;
return Json(stu) ;
}
}
The above verified the role based api permission, and if you prefer to use delegate api permission, you can set the api like this
[ApiController]
[Authorize]
public class HomeController : ControllerBase
{
[HttpGet]
[RequiredScope("User.Read")]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
Have you looked at the useWhen mechanism, which lets you apply a type of authentication to a particular condition - eg Path + Method:
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(new PathString("/api/mypath")) &&
ctx.Request.Method != "OPTIONS",
api => api.UseAuthentication()
);
My experimental .Net Core API does this and may give you some ideas. My objective was to write APIs in different technologies in the same way, and to take closer control over OAuth / claims processing.
In a real API it of course makes sense to keep code simple as much as you can, and of course to use proper libraries for any real security / crypto.
With respect to asp.net core identity management, we have a requirement to change the Microsoft ClientId and ClientSecret after our asp.net core app has started and, therefore, not in startup.cs. We have various identity management logins working fine with, for example this for Microsoft Azure:
.AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.CorrelationCookie.HttpOnly = true;
microsoftOptions.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
microsoftOptions.ClientId = "removed";
microsoftOptions.ClientSecret = "removed";
})
We now need to change the ClientId and ClientSecret dynamically after the core application has started and what we can't figure out is how to access this from the services collection later in other pages so we can update them.
Any help appreciated.
Thanks.
ASP.NET Core provides IAuthenticationSchemeProvider interface to dynamically add/remove authentication schemes at runtime. You can inject this interface and add Microsoft Account auth schemes after the app has started.
Using Microsoft's demo app as reference, here's a basic implementation:
public class DynamicAuthController: ControllerBase
{
private IAuthenticationSchemeProvider _schemeProvider;
private IOptionsMonitorCache<MicrosoftAccountOptions> _optionsCache;
public DynamicAuthController(IAuthenticationSchemeProvider schemeProvider, IOptionsMonitorCache<MicrosoftAccountOptions> optionsCache)
{
_schemeProvider = schemeProvider;
_optionsCache = optionsCache;
}
[HttpPost]
public ActionResult Add()
{
var schemeName = "MicrosoftCustom1"; // must be unique for different schemes
var schemeOptions = new MicrosoftAccountOptions
{
ClientId = "ididid", // fetch credentials from another service or database
ClientSecret = "secretsecret",
CorrelationCookie =
{
HttpOnly = true,
SecurePolicy = CookieSecurePolicy.Always
}
};
var scheme = new AuthenticationScheme(schemeName, displayName:null, typeof(MicrosoftAccountHandler));
_schemeProvider.TryAddScheme(scheme);
_optionsCache.TryAdd(
schemeName,
schemeOptions
);
return Ok();
}
}
Context: Got an API running with a simple /auth call that expects email, password and some sort of db identifier. Which then returns a JWT token. This token can be used to request the other calls and know which database to access. The client is now in UWP which handles the UI and does the calls to the API. Not using Azure Api Management for now and not using the Microsoft Identity platform. Just a regular password hash check.
Recently, we wanted to switch from UWP to a Blazor WASM (client only) but haven't really found any suitable support to work with Bearer tokens and the documentation steers us towards four options.
AAD
AAD B2C
Microsoft Accounts
Authentication library (?)
Not all our users have Office 365 accounts.
Kind of lost in this new "Blazor space" since it's very different from our WPF & UWP projects and it doesn't seem to be fully documented yet.
Thanks.
Update code on request
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
// Local storage access
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddTransient<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddTransient<IAccessTokenProvider, ApiTokenProvider>();
builder.Services
.AddHttpClient<IMambaClient, MambaClient>(client => client.BaseAddress = _baseUri)
.AddHttpMessageHandler(sp => sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>()
.ConfigureHandler(new[] { _apiEndpointUrl }));
await builder.Build().RunAsync();
}
ApiTokenProvider.cs
public class ApiTokenProvider : IAccessTokenProvider
{
private readonly ILocalStorageService _localStorageService;
public ApiTokenProvider(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
public async ValueTask<AccessTokenResult> RequestAccessToken()
{
var token = await _localStorageService.GetItemAsync<string>("Token");
AccessTokenResult accessTokenResult;
if (!string.IsNullOrEmpty(token))
{
accessTokenResult = new AccessTokenResult(AccessTokenResultStatus.Success, new AccessToken() { Value = token, Expires = new DateTimeOffset(DateTime.Now.AddDays(1)) }, "/");
}
else
{
accessTokenResult = new AccessTokenResult(AccessTokenResultStatus.RequiresRedirect, new AccessToken() { Value = token, Expires = new DateTimeOffset(DateTime.Now.AddDays(1)) }, "/login");
}
return await new ValueTask<AccessTokenResult>(accessTokenResult);
}
public ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options)
{
throw new NotImplementedException();
}
}
New question: How will I be able to call POST /auth now if this would work? I would get an error since I don't have a token yet for this TypedClient and adding another typed client isn't possible since I cannot give it a different name?
In the Azure B2C, I used to be able to get a "groups" claim in my JWT tokens by following Retrieving Azure AD Group information with JWT:
Open the old-school Azure manager (https://manage.windowsazure.com)
Register my application with B2C
Download the B2C manifest for the application
In the manifest, change the "groupMembershipClaims" entry to
"groupMembershipClaims": "SecurityGroup",
Upload the changed B2C manifest again
The problem
This has worked well in the past (about a month ago, I believe...) but it doesn't anymore. See below for details...
What I've tried sofar
Plan A: Use Azure Manager
Follow the known-good recipe above.
Unfortunately that doesn't work anymore - I get the following error when this client tries to authenticate me with B2C:
AADB2C90068: The provided application with ID '032fe196-e17d-4287-9cfd-25386d49c0d5' is not valid against this service. Please use an application created via the B2C portal and try again"
OK, fair enough - they're moving us to the new Portal.
Plan B: Use Azure Portal
Follow the good old recipe, using the new Portal.
But that doesn't work either - when I get to the "download manifest" part, I cannot find any way to access the manifest (and Googling tells me it's probably gone for good...).
Plan C: Mix Azure Portal and manager
Getting a little desperate, I tried mixing plans A and B: register the app using the new Portal, then change the manifest using the old Azure Manager.
But no luck - when I try to upload the manifest, it fails with the message
ParameterValidationException=Invalid parameters provided; BadRequestException=Updates to converged applications are not allowed in this version.
Plan Z: Use the Graph API to retrieve group membership data
Just give up the "group" claim - instead, whenever I need group info, just query the B2C server using the Graph API.
I really, really don't want to do this - it would ruin the self-contained-ness of the access token, and make the system very "chatty".
But I've included it as a plan Z here, just to say: yes, I know the option exists, no I haven't tried it - and I'd prefer not to.
The question:
How do I get the "group" claim in my JWT token these days?
Plan Z it is I'm afraid. I don't know why they don't return it, but it's currently marked as planned on their Feedback Portal (it's the highest rated item).
This is how I'm doing it. Querying the groups when the user is authenticated, you can do it your way as well - just query as and when you need to. Depends on your use case.
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = new PathString("/account/unauthorised"),
CookieSecure = CookieSecureOption.Always,
ExpireTimeSpan = TimeSpan.FromMinutes(20),
SlidingExpiration = true,
CookieHttpOnly = true
});
// Configure OpenID Connect middleware for each policy
app.UseOpenIdConnectAuthentication(CreateOptionsFromPolicy(Globals.SignInPolicyId));
}
private OpenIdConnectAuthenticationOptions CreateOptionsFromPolicy(string policy)
{
return new OpenIdConnectAuthenticationOptions
{
// For each policy, give OWIN the policy-specific metadata address, and
// set the authentication type to the id of the policy
MetadataAddress = string.Format(Globals.AadInstance, Globals.TenantName, policy),
AuthenticationType = policy,
AuthenticationMode = AuthenticationMode.Active,
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientIdForLogin,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = AuthenticationFailed,
SecurityTokenValidated = SecurityTokenValidated
},
Scope = "openid",
ResponseType = "id_token",
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
}
};
}
private async Task SecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> token)
{
var groups = await _metaDataService.GetGroups(token.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value);
if (groups?.Value != null && groups.Value.Any())
{
foreach (IGroup group in groups.Value.ToList())
{
token.AuthenticationTicket.Identity.AddClaim(
new Claim(ClaimTypes.Role, group.DisplayName, ClaimValueTypes.String, "GRAPH"));
}
}
}
// Used for avoiding yellow-screen-of-death
private Task AuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
}
My GetGroups method just queries the getMemberGroups method on the Users API
Then I have a simple helper method to determine whether the user is in a role:
public static bool UserIsInRole(IPrincipal user, string roleName)
{
var claims = user.Identity as ClaimsIdentity;
if (claims == null) return false;
return claims.FindAll(x => x.Type == ClaimTypes.Role).Any(x => x.Value == roleName);
}
We have an ASP.NET MVC application that is authenticating without issue against IdentityServer3, however the web API part of the application using ApiController's start to fail if the user waits before proceeding with AJAX functionality after about 3 minutes (before 3 mins everything seems fine).
The errors seen in Chrome are:
XMLHttpRequest cannot load
https://test-auth.myauthapp.com/auth/connect/authorize?client_id=ecan-farmda…gwLTk5ZjMtN2QxZjUyMjgxNGE4MDg2NjFhZTAtOTEzNi00MDE3LTkzNGQtNTc5ODAzZTE1Mzgw.
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://test.myapp.com' is therefore not allowed
access.
On IE I get the following errors:
SCRIPT7002: XMLHttpRequest: Network Error 0x4c7, The operation was
canceled by the user.
Looking at IdentityServer3's logs I'm seeing entries like so:
2015-08-10 16:42 [Warning]
(Thinktecture.IdentityServer.Core.Configuration.Hosting.CorsPolicyProvider)
CORS request made for path: /connect/authorize from origin:
http://test.myapp.com but rejected because invalid CORS path
In the IdentityServer3 web application I'm giving clients AllowedCorsOrigins:
Thinktecture.IdentityServer.Core.Models.Client client = new Thinktecture.IdentityServer.Core.Models.Client()
{
Enabled = configClient.Enabled,
ClientId = configClient.Id,
ClientName = configClient.Name,
RedirectUris = new List<string>(),
PostLogoutRedirectUris = new List<string>(),
AllowedCorsOrigins = new List<string>(),
RequireConsent = false, // Don't show consents screen to user
RefreshTokenExpiration = Thinktecture.IdentityServer.Core.Models.TokenExpiration.Sliding
};
foreach (Configuration.RegisteredUri uri in configClient.RedirectUris)
{
client.RedirectUris.Add(uri.Uri);
}
foreach (Configuration.RegisteredUri uri in configClient.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(uri.Uri);
}
// Quick hack to try and get CORS working
client.AllowedCorsOrigins.Add("http://test.myapp.com");
client.AllowedCorsOrigins.Add("http://test.myapp.com/"); // Don't think trailing / needed, but added just in case
clients.Add(client);
And when registering the service I add a InMemoryCorsPolicyService:
app.Map("/auth", idsrvApp =>
{
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<AuthContext>(resolver => AuthObjects.AuthContext));
factory.Register(new Registration<AuthUserStore>());
factory.Register(new Registration<AuthRoleStore>());
factory.Register(new Registration<AuthUserManager>());
factory.Register(new Registration<AuthRoleManager>());
// Custom user service used to inject custom registration workflow
factory.UserService = new Registration<IUserService>(resolver => AuthObjects.AuthUserService);
var scopeStore = new InMemoryScopeStore(Scopes.Get());
factory.ScopeStore = new Registration<IScopeStore>(scopeStore);
var clientStore = new InMemoryClientStore(Clients.Get());
factory.ClientStore = new Registration<IClientStore>(clientStore);
var cors = new InMemoryCorsPolicyService(Clients.Get());
factory.CorsPolicyService = new Registration<ICorsPolicyService>(cors);
...
var options = new IdentityServerOptions
{
SiteName = "Authentication",
SigningCertificate = LoadCertificate(),
Factory = factory,
AuthenticationOptions = authOptions
};
...
});
I do note that the IdentityServer3 log entries say "CORS request made for path: /connect/authorize" rather than "CORS request made for path: /auth/connect/authorize". But looking through the IdentityServer3 source code suggests this probably isn't the issue.
Perhaps the InMemoryCorsPolicyService isn't being picked up?
Any ideas of why things aren't working for the AJAX called ApiController?
Thinktecture.IdevtityServer3 v1.6.2 has been installed using NuGet.
Update
I'm having a conversation with the IdentityServer3 developer, but am still having an issue reaching a resolution. In case it helps:
https://github.com/IdentityServer/IdentityServer3/issues/1697
Did you try adding https url also?- client.AllowedCorsOrigins.Add("https://test.myapp.com");
The documentation of IdentityServer says you should configure it on the client:
AllowedCorsOrigins = ... // Defaults to the discovery, user info, token, and revocation endpoints.
https://docs.duendesoftware.com/identityserver/v6/reference/options/#cors
CORS is a nightmare!
It's a browser thing which is why you're witnessing different behaviour in IE than in Chrome.
There are (at least) two ways that CORS is configured on the server. When a client makes a request with the Origin header you have to tell the server whether or not to accept it -- if accepted then the server adds the Access-Control-Allow-Origin header to the response for the browser.
In MVC / webAPI you have to add CORS services, set a CORS policy, and then .UseCors something like this:
builder.Services.AddCors((options =>
{
if (settings.AllowedCorsOrigins.Length > 0)
{
options.AddDefaultPolicy(builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().WithOrigins(settings.AllowedCorsOrigins);
});
}
if (isDevelopment)
{
options.AddPolicy("localhost", builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed((string origin) => { return origin.Contains("localhost"); }); });
}
});
and
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.UseCors("localhost");
}
Typically, you want the list of allowed hosts as an array of strings in your appsettings.json. And watch out for the boobytrap with SetIsOriginAllowedToAllowWildcardSubdomains.
As well as this, IdentityServer has its own additional CORS settings which are applied in addition to the standard MVC/webAPI settings. These are in the ClientCorsOrigin table and this doesn't support wildcard subdomains. You can sidestep this whole boobytrap by implementing your own ICorsPolicyService to use the same settings from your appsettings.json something like this
public class CorsPolicyService : ICorsPolicyService
{
private readonly CorsOptions _options;
public CorsPolicyService(IOptions<CorsOptions> options)
{
_options = options.Value;
}
private bool CheckHost(string host)
{
foreach (string p in _options.AllowedCorsOrigins)
{
if (Regex.IsMatch(host, Regex.Escape(p).Replace("\\*", "[a-zA-Z0-9]+"))) // Hyphen?
{
return true;
}
}
return false;
}
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(CheckHost(origin));
}
}