How to use dynamic ApiSecret in securing an api with IdentityServer middleware - c#

I'm securing an API with the IdentityServer middle-ware talking to an identity server.
In the API I have code similar to:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(
options =>
{
options.Authority = identityServerSettings.AuthorityUri;
options.ApiName = identityServerSettings.ApiName;
options.ApiSecret = secret;
}
);
I want to setup the middle-ware with an ApiSecret, but I want to get this secret from hashicorp's vault.
I want to configure the middle-ware so that it retrieves a values and sets the ApiSecret on each request.
Something like:
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(
options =>
{
options.Authority = identityServerSettings.AuthorityUri;
options.ApiName = identityServerSettings.ApiName;
options.ApiSecret = () => GetSecret();
}
);
The intention for this would be to enable service restart-less key rotation by hashicorp's vault.
Both this API and the IdentityServer would request the secret from vault and this would allow the rotation of secrets without the restarting of the API or IdentityServer services.
Is this possible? How would one do it?

Related

IdentityServer and CertificateValidation ASP.NET Core

I'm building an ASP.NET Core 3.1 Web API. For Authentication I'm using right now IdentyServer4. Now I got the additional requirement to apply Mutual TLS. When applying this this results in the following code in my Startup.cs (using: IdentityServer4.AccessTokenValidation (3.0.1) and Microsoft.AspNetCore.Authentication.Certificate (3.1.3)):
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "<baseaddress>";
options.ApiName = "<API>";
});
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
...
}
Now I'm facing the issue that my ClaimsPrincipal is overwritten by the Microsoft.AspNetCore.Authentication.Certificate. This is not desired because we use the claims from IdentityServer4 for allowing/denying functionality.
What's recommended in this situation?
These two lines are the problem in your code
services.AddAuthentication("Bearer")
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
You are overriding the default authentication scheme to certificate.
You should just add your certificate authentication
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "<baseaddress>";
options.ApiName = "<API>";
})
.AddCertificate(options =>
{
...
}
And use this code to authenticate with certificate
httpContext.AuthenticateAsync(CertificateAuthenticationDefaults.AuthenticationScheme);

ASPNETCore SignalR authentication with Reference token

We are using ASPNETCore.SignalR 1.1.0 inside our Web API (netcoreapp2.2).
Authentication : We are using IdentityServer4 authentication for our project.
Startup.cs
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://IdentityServerDomainURL:8081/";
options.RequireHttpsMetadata = false;
options.ApiName = "name";
options.ApiSecret = "secret";
});
In WebAPI application we have added our SignalR Hub.
A JavaScript client connects to this Hub.
Following is the code of JS client for connecting to the hub.
var connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:52177/SignalRHub/",
{
accessTokenFactory: () => "referencetokenValue"
}).build();
The JS client is passing the Reference token at the time of connecting to the Hub.
We need to use Reference token authentication for SignalR in the WebAPI project.
In Microsoft`s site only JWT token authentication documentation for SignalR is provided. Did not find any document anywhere regarding Reference tokens.
Need help in adding the configuration for reference token authentication in startup.cs file.
Found the solution here.
Any token coming in query string can be added in the request headers as follows:-
app.Use(async (context, next) =>
{
if (context.Request.Path.Value.StartsWith("/SignalRHub/"))
{
var bearerToken = context.Request.Query["access_token"].ToString();
if (!String.IsNullOrEmpty(bearerToken))
context.Request.Headers.Add("Authorization", new string[] { "bearer " + bearerToken });
}
await next();
});
The above code has to be added in the Configure function of startup class.

How add two different tokens in ASP.NET Core Web API

I need the Authorize attribute in our Controller can accept two different tokens.
One token, is provided from one private ADFS, and other token is provided from AzureAd.
Several Ionic clients go to over ADFS, other Ionic clients go to over Azure AD
My dev scenario: ASP.NET Core 2.2 Web API
My actual startup.cs (abbreviated)
ConfigureService()
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer((options =>
{
options.Audience = Configuration["Adfs:Audience"];
options.Authority = Configuration["Adfs:Issuer"];
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
}));
}
I need here the other Authentication with AzureAD. How?
The Configure method of Startup.cs
Configure(…)
{
app.UseAuthentication()
}
With this code, only can access the ADFS Token and this users, can obtains result from the controllers. However, the AzureAD user's can't obtain access
I don't know how make this code for double token authorization, and our controllers can response if one token is from ADFS or other token is from AzureAD
You can set multiple JWT Bearer Authentication with different schema name :
services.AddAuthentication()
.AddJwtBearer("ADFS",options =>
{
options.Audience = Configuration["Adfs:Audience"];
options.Authority = Configuration["Adfs:Issuer"];
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false
};
})
.AddJwtBearer("AAD", options =>
{
//AAD jwt validation configuration
});
If you want to make your controller/action to accept two jwt tokens , tokens from AAD or ADFS are ok to access your controller/action , you can make a policy to let both the AAD and ADFS authentication schemes tried to authenticate the request :
services
.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("AAD", "ADFS")
.Build();
});
In addition , if you want to know which schema the token is from , you can check the particular claim in user's identity , or directly add authentication schema value to user claims in events :
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
OnTokenValidated = (context) =>
{
var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
//add your custom claims here
claimsIdentity.AddClaim(new Claim("schema", "AAD"));
return Task.FromResult(0);
}
};
And get in action after authentication :
var result = User.Claims.Where(c=>c.Type=="schema").FirstOrDefault().Value;

How to integrate swagger with Azure Active Directory OAuth

I'm trying to setup Swagger in my AspNetCore 2.1 application using Azure Active Directory V2 but I cannot seem to get it right. I am able to configure the setup so that swagger prompts, redirects and successfully authenticates my client/user but when passing the bearer token to the server results in the error Bearer error="invalid_token", error_description="The signature is invalid". I have created a GitHub repository with the project I am trying to get work with all its configuration (https://github.com/alucard112/auth-problem)
I have managed to get the V1 endpoint working, by setting the resource to the Client Id of the AAD app, which results in the JWT token having the 'aud' set to the app client Id. In the V2 endpoint the 'aud' is being set to what I think is the Graph API resource '00000003-0000-0000-c000-000000000000'. I believe this is my problem at the moment, although am not 100 % sure. The V2 endpoints don't seem to have a way to define the audience like the V1 did unless of course there is some oversight from my side.
My Startup file is structured as follows:
The authentication is setup as the following:
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Authority = $"https://login.microsoftonline.com/{tenantId}";
options.TokenValidationParameters = new TokenValidationParameters
{
// In multi-tenant apps you should disable issuer validation:
ValidateIssuer = false,
// In case you want to allow only specific tenants,
// you can set the ValidIssuers property to a list of valid issuer ids
// or specify a delegate for the IssuerValidator property, e.g.
// IssuerValidator = (issuer, token, parameters) => {}
// the validator should return the issuer string
// if it is valid and throw an exception if not
};
});
And the swagger is setup as follows:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Title = "Protected Api",
});
c.OperationFilter<SecurityRequirementsOperationFilter>();
//IMATE - StevensW
// Define the OAuth2.0 scheme that's in use (i.e. Implicit Flow)
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize",
TokenUrl = $"https://login.microsoftonline.com/common/{tenantId}/v2.0/token",
Scopes = new Dictionary<string, string>
{
{ "openid", "Unsure" },
{ "profile", "Also Unsure" }
}
});
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.OAuthClientId(Configuration.GetValue<string>("AzureAd:ClientId"));
c.OAuthAppName("Protected API");
// c.OAuthUseBasicAuthenticationWithAccessCodeGrant();
// NEVER set the client secret here. It will ve exposed in the html of the swagger page if you "view source" and its not needed for OpenID Auth
// c.OAuthClientSecret(Configuration.GetValue<string>("AzureAd:ClientId"));
});
I am hoping to configure the swagger UI to use AAD's V2 endpoint and allow for a multi-tenant login that allows successfully authenticated API calls to be executed. Any help or direction would be greatly appreciated.
I ended up fixing the problem I was having. Working through this post helped me understand my mistakes.
The first mistake was my actual AAD app registration. I had not set a scope for the application under "Expose an API". Because they deprecated the resource property in V2, the way you would set the resource was to create a scope with the format api"//{application ID}/{scope_name}. After I made this change my AAD application was now correctly configured.
After that, I needed to add an additional section to my startup file:
return services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
// This is an Azure AD v2.0 Web API
options.Authority += "/v2.0";
// The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
options.TokenValidationParameters.ValidAudiences = new string[] { options.Audience, $"api://{options.Audience}" };
options.TokenValidationParameters.ValidateIssuer = false;
});
Note: the link above provided an alternative solution to turning off the validation of the issuer if anyone is interested.
My AppSettings file was also simplified by only needing to define the Instance, TenantId, and ClientId.
Then from a swagger perspective, I just needed to add an additional scope to the security definition matching the one I created in my AAD application.
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
Scopes = new Dictionary<string, string>
{
{ "openid", "Sign In Permissions" },
{ "profile", "User Profile Permissions" },
{ $"api://{clientId}/access_as_user", "Application API Permissions" }
}
});
After these changes my application is now working as expected.
for v2 endpoint, update the accessTokenAcceptedVersion in Manifest of AAD from null to 2. It will work.

Can't protect internal api using IdentityServer 4 (500 error)

A bit of background, I have an IdenityServer 4 project that I use to protect access to an mvc project that I have (Using ASP.NET Identity).
Now what I also wanted was an api that is protected via client credentials that returns some information.
What I did was make a new core api project and this was working fine with the client protection, however, I wanted to move the api so it was within IdenityServer.
e.g. localhost:5000/api/info/getinfo
Now I have moved the code over I get a 500 error when I use the attribute [Authorize(AuthenticationSchemes = "Bearer")]
I can use the DiscoveryClient to get a successful token using the credentials but can't with any request unless they are not authorized.
So in ID I set up my start up like this:
services.AddMvc();
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetSection("Authority").Value;
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
And then for my api call that is protected I tag it with: [Authorize(AuthenticationSchemes = "Bearer")]
but this returns me a 500 error, now when I use the tag: [Authorize] it works but that's because the user is logged into the mvc app and the response is an html page and not the json object i want.
At the moment I'm using a unit test to hit the api and the code looks like this:
var client = new HttpClient();
var disco = DiscoveryClient.GetAsync("https://localhost:5000").Result;
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("IdentityInfoApi").Result;
client.SetBearerToken(tokenResponse.AccessToken);
var response = client.GetAsync("https://localhost:5000/api/info/getinfo").Result;
if (!response.IsSuccessStatusCode)
{
var userResult = response.Content.ReadAsStringAsync().Result;
var result = JsonConvert.DeserializeObject<PagedUserList>(userResult);
Assert.NotNull(result);
}
Is there something wrong with my setup of ID, the client code or can you not use ID in this way?
Thank's for your help
After a lot of playing around I believe I found the fix.
You must define AddAuthentication() before AddIdentity() or in other words you must configure the api before Identity Server
It's fine to do it any way round if your api is external but not if it is within the Identity Server app it'self.
My new code looks like this:
//Configure api
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "IdentityInfoApi";
});
//end
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.Configure<AuthMessageSenderOptions>(Configuration.GetSection("SMTP"));
services.AddMvc();
// Configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryClients(Config.GetClients(Configuration))
.AddInMemoryApiResources(Config.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
Hope this helps anyone else

Categories