How to validate a token issued by IdentityServer3 using WebApi - rest

I need help to understand what is happening in the following scenario.
I have an architecture drawn as below.
0 - Presentation
1 - Authentication Server (Using IdentityServer3)
2 - Isolated WebApi using OData (Without Default Controllers by AspNet.Identity)
3 - Business Services
4 - Custom Repository
5 - Repository generic and data access layer
The layers of number 0 and 2 necessarily perform their authentication through layer number 1.
Authentication is working perfectly for my number 1 presentation layer using implicit flow.
However, the WebApi number 2 small services layer is failing to authenticate by passing the token provided by the authentication server.
I received the following message.
{
"odata.error": {
"code": "",
"message": {
"lang": "en-US",
"value": "Authorization has been denied for this request."
}
}
}
My ConfigAuth in WebApi
public static void ConfigureAuth(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
ClientId = "WebApi",
ClientSecret = "secret",
Authority = "http://localhost:35373/identity", // TODO: Vai mudar com a versão final
RequiredScopes = new[] { "read" , "write" , "offline_access" }
});
app.UseWebApi(WebApiConfig.Register());
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
This is my request token from identityserver3
{
"access_token": "5f4ca4bc0514158409903080af5c1081",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "a8cd624dd3169a607bdb24ad0833d1b3"
}

Related

protect asp.net web api 2 project with identity server 4

I have an asp.net web api 2 project with .Net framework 4.8 and a centralized Identity Server 4 project. I want to validate jwt/access token generated from IS4 in my web api 2 project. I can understand its a duplicate question but somehow I am unable to find any suitable help and I am not sure what's missing. I have used IdentityServer3.AccessTokenValidation for token validation in web api project.
Startup.cs
using IdentityServer3.AccessTokenValidation;
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(WebApplicationApiNew.Startup))]
namespace WebApplicationApiNew
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44373",
RequiredScopes = new[] { "api1" },
});
}
}
}
Calling this API with a valid JWT bearer token still gives 401:
[Authorize]
[HttpPost]
public String GetName1()
{
if (User.Identity.IsAuthenticated)
{
var identity = User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
}
return "Valid";
}
else
{
return "Invalid";
}
}
Error details:
2021-07-24 20:41:25.4133|DEBUG|Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware|Authentication failed
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidAudienceException: IDX10214: Audience validation failed. Audiences: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'. Did not match: validationParameters.ValidAudience: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]' or validationParameters.ValidAudiences: '[PII is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
at Microsoft.IdentityModel.Tokens.Validators.ValidateAudience(IEnumerable`1 audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateAudience(IEnumerable`1 audiences, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateTokenPayload(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
at Microsoft.Owin.Security.Jwt.JwtFormat.Unprotect(String protectedText)
at Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationHandler.<AuthenticateCoreAsync>d__3.MoveNext()
The error shows:
IDX10214: Audience validation failed
If your JWT token contains an "aud" claim, the authentication middleware
is attempting to validate the audience claim, but there is no audience specified in your options. Can you try one of the following:
Set an audience in the options:
Audience = "[your audience]"
Disable audience validation in the options:
TokenValidationParameters.ValidateAudience = false;
Refer to the identity server documentation for more details:
https://docs.identityserver.io/en/latest/topics/apis.html
I found solution to my problem. IS3 requires aud claim (with /resources in url) to be present in jwt/access token in order to validate the token. But on the other side IS4 has stopped emitting the aud claim by default. So we need to explicitly set IS4 server to emit aud claim to support legacy/old access token validation with IdentityServer3.AccessTokenValidation.
services.AddIdentityServer(options =>
{
...
options.EmitStaticAudienceClaim = true; // <- for older access token validation
})
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
...

Cloud Run Service requests always fail with 401 when using JWT returned for another service. The same request works with JWT returned for end-user

I have an app consisting of multiple Cloud Run Services. Each service is open only to a specific set of other services.
Let's say I have 2 services with URLs https://api-service-123.run.app and https://data-provider-service-123.run.app. The first service also has a custom URL https://api.my.domain.
api-service needs access to data-provider-service. So, following the documentation, I created two per-service user-managed service-accounts api-service-account#domain and data-provider-service-account#domain. I added api-service-account#domain into data-provider-service with Cloud Run Invoker role.
Now, I have trouble accessing the account with ID Token returned from Google. I tried several ways. Firstly, I utilized slightly adjusted code from the docks
export const getToken = async (url: string, targetAUD?: string) => {
const auth = new GoogleAuth();
const request = async () => {
if (!targetAUD) {
targetAUD = new URL(url).origin;
}
console.info(`request ${url} with target audience ${targetAUD}`);
const client = await auth.getIdTokenClient(targetAUD);
const res = await client.request({url});
console.info(res.data);
return res.data;
}
try {
return await request();
} catch (err) {
console.error(err);
}
}
When the function above is called as getToken('http://data-provider-service-123.run.app');, the request fails with 401 Unauthorized.
I also tried to call it as getToken('https://data-provider-service-123.run.app'); and got a response 403 Forbidden. The following entry is added to data-provider-service logs for each such request
{
httpRequest: {9}insertId: "60fcb7e0000c12346fe37579"
logName: "projects/my-project/logs/run.googleapis.com%2Frequests"
receiveTimestamp: "2021-07-25T01:01:20.806589957Z"
resource: {2}
severity: "WARNING"
textPayload: "The request was not authorized to invoke this service. Read more at
https://cloud.google.com/run/docs/securing/authenticating"
timestamp: "2021-07-25T01:01:20.800918Z"
trace: "projects/my-project/traces/25b3c13890aad01234829711d4e9f25"
}
I also tried to obtain and use JWT from compute metadata server (also a way advertised in the docs) by running curl "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=http://data-provider-servive-123.run.app" \ -H "Metadata-Flavor: Google".
When I execute this code in Cloud Shell, I get JWT that looks like this
{
"iss": "accounts.google.com",
"azp": "123456789-9r9s1c4alg36erliucho9t52n12n6abc.apps.googleusercontent.com",
"aud": "123456789-9r9s1c4alg36erliucho9t52n12n6abc.apps.googleusercontent.com",
"sub": "106907196154567890477",
"email": "my_email#gmail.com",
"email_verified": true,
"at_hash": "dQpctkQE2Sy1as1qv_w",
"iat": 1627173901,
"exp": 1627177501,
"jti": "448d660269d4c7816ae3zd45wrt89sb9f166452dce"
}
Then I make a request using postman to https://data-provider-service/get-data with a header Authorization: "Bearer <the_jwt_from_above>" and everything works fine
However, if I make the same request from my api-service, the JWT returned is
{
"aud": "http://data-provider-service-123.run.app",
"azp": "111866132465987679716",
"email": "api-service-account#domain",
"email_verified": true,
"exp": 1627179383,
"iat": 1627175783,
"iss": "https://accounts.google.com",
"sub": "111866132465987679716"
}
If I make the same request with the postman and place this JWT into the Authorization header, the 401 Unauthorized is returned.
I have spent this week trying to solve this issue. I triple-checked the permissions, redeployed the services several times, but nothing helps.
I changed http to https in the URL of the target service when asking computing metadata server, and looks like it solved the problem. Requesting token like this with google-auth-library still fails with 403 error.
I'll update that answer if the solution is sustainable.

Microprofile JWT responding with 401 all the time

I'm having an issue trying to get Microprofile JWT working for my REST resources.
I'm able to build a JWT so when decoded on jwt.ie it is
{
"kid": "Oh1YDQopers_qYMU4zQCmAf0UsFVD5D0NmkFE79s2q0",
"typ": "JWT",
"alg": "RS256"
}
{
"token_type": "Bearer",
"sub": "user12",
"upn": "user12",
"groups": [
"ADMIN",
"USER"
],
"jti": "a27582fc-21e2-4365-b485-ed7193606d8b",
"iss": "http://www.testissuer.com",
"exp": 1617226928,
"iat": 1617219728
}
My Application class is annotated with
#LoginConfig(authMethod = "MP-JWT")
#DeclareRoles({"USER", "SUPERUSER", "ADMIN"})
#ApplicationPath("/")
public class MyApplication extends Application {
Resource class is annotated with
#Path("/my")
#PermitAll
#RequestScoped
public class MyResource {
#Inject
#Claim(standard = Claims.groups)
private Set<String> groups;
#GET
#Produces(MediaType.TEXT_PLAIN)
#RolesAllowed("USER")
public String getString() {
if (groups != null) {
return "groups.size(): " + groups.size();
}
else {
return "groups is null";
}
}
}
The ear file which includes this war includes a META-INF/microprofile-config.properties file with entry:
mp.jwt.verify.issuer=http://www.testissuer.com
To test this I'm generating a fresh token and setting the Authorization header to the encoded JWT and calling GET /my which is returning a 401 response. Added Bearer before the encoded token makes no difference. The WWW-Authenticate header on the 401 response looks like it's looking for a Basic realm value.
If I take out the #RolesAllowed("USER") line then the response I get back is "groups is null" so it's like the injection is failing or cannot be mapped to the "groups" claim in the JWT.
Anyone run into this before?
#PermitAll annotation on resource class was wrong, using it prevents principal data from being injected. Fix was to use #DenyAll on class and #RolesAllowed or #PermitAll at method level.

The audience is invalid error

I have 3 projects 1- Javascript SPA 2- Web API Project, 3- IdentityServer with EF Core
I started debugging API and Identity Server and successfully get the jwt token but, when I try to get value from API method which has Authorize Attribute I get an error:
WWW-Authenticate →Bearer error="invalid_token", error_description="The audience is invalid"
I could not found any property about audience in auth options. This is my configuration in API project
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
ApiSecret="secret",
Authority = "http://localhost:5000",
ApiName="fso.Api",
RequireHttpsMetadata = false,
});
And my Config.cs file in Identity
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource()
{
Name = "fso.Api",
DisplayName = "feasion API",
Scopes =
{
new Scope("api1"),
new Scope(StandardScopes.OfflineAccess)
},
UserClaims =
{
JwtClaimTypes.Subject,
JwtClaimTypes.EmailVerified,
JwtClaimTypes.Email,
JwtClaimTypes.Name,
JwtClaimTypes.FamilyName,
JwtClaimTypes.PhoneNumber,
JwtClaimTypes.PhoneNumberVerified,
JwtClaimTypes.PreferredUserName,
JwtClaimTypes.Profile,
JwtClaimTypes.Picture,
JwtClaimTypes.Locale,
JwtClaimTypes.IdentityProvider,
JwtClaimTypes.BirthDate,
JwtClaimTypes.AuthenticationTime
}
}
};
}
public static List<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile(),
};
}
// client want to access resources (aka scopes)
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "fso.api",
AllowOfflineAccess=true,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedScopes =
{
StandardScopes.OfflineAccess,
"api1"
}
}
};
}
}
See here for what this claim is about:
The aud (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the aud claim when this claim is present, then the JWT MUST be rejected....
So your API's name must exist in the aud claim for the JWT to be valid when it is validated by the middleware in your API. You can use jwt.io to look at your token by the way, that can be useful to help make sense of it.
In order to have IdentityServer to add your API's name to the aud claim your client code (which is attempting to get a resource from the API and therefore needs an access token) should request a scope from your API. For example like this (from an MVC client):
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
Authority = Configuration["IdpAuthorityAddress"],
ClientId = "my_web_ui_id",
Scope = { "api1" },
//other properties removed...
});
To avoid the error, audience should be consistently added in 4 places
In My (e.g. MVC) client as custom Scope.
In API application as ApiName
In IdentityServer Clients configuration as AllowedScope
In API Resources configuration as ApiResource
See details ( previously available in IdentityServer4 wiki):
When configuring a new API connection in identityServer4, you can get an error:
WWW-Authenticate: Bearer error="invalid_token",
error_description="The audience is invalid"
To avoid the error, Audience should be consistently added in 4 places
In My (e.g. MVC) client as custom Scope :
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
Authority = Configuration["IdpAuthorityAddress"],
ClientId = "my_web_ui_id",
Scope = { "openid", "profile", "offline_access", "MyApi" },
//other properties removed for brevity...
});
In API application as ApiName
//Microsoft.AspNetCore.Builder.IdentityServerAuthenticationOptions
var identityServerAuthenticationOptions = new IdentityServerAuthenticationOptions()
{
Authority = Configuration["Authentication:IdentityServer:Authority"],
RequireHttpsMetadata = false,
EnableCaching = false,
ApiName = "MyApi",
ApiSecret = "MyApiSecret"
};
In IdentityServer \IdentityServerHost\Configuration\Clients.cs
(or corresponding Clients entry in the database)
var client = new Client
{
ClientId = clientId,
//other properties removed for brevity...
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
//IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.OfflineAccess, "MyApi",
},
};
In IdentityServer \IdentityServerHost\Configuration\Resources.cs (or corresponding ApiResource entry in the database) as apiResource.Scopes
var apiResource = new ApiResource
{
Name = "MyApi",
ApiSecrets =
{
new Secret("MyApiSecret".Sha256())
},
UserClaims =
{
JwtClaimTypes.Name,
JwtClaimTypes.Profile,
},
};
In your app configuration file in AD configuration section add "Audience" line:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "<-- Enter the Client Id -->",
"Audience": "<-- Enter the Client Id -->",
"TenantId": "<-- Enter the tenantId here -->"
}
In my case "ClientId" & "Audience" was the same.
P.S.: And if after that you'll see
IDW10201: Neither scope or roles claim was found in the bearer token
Add another line to AD configuration:
"AllowWebApiToBeAuthorizedByACL": true
More here
In IdentityServer had to add claim "aud" to the jwt Token. In Order to do that under .AddJwtBearer("Bearer", options => options.Audience="invoice" and set ApiResource
Reference Link https://identityserver4.readthedocs.io/en/latest/topics/resources.html#refresources
public static readonly IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("invoice", "Invoice API")
{
Scopes = { "invoice.read", "invoice.pay", "manage" }
}
};
}

Issues with GetProfileDataAsync after upgrade to IdentityServer3 v2.5 from v1.6.3

We've been running IdentityServer3 v1.x successfully over the past year, but have now upgraded to v2.5 from v1.6.3.
We have a custom UserService that implements the IUserService, so this was modified for the new context parameters and we are able to login, but are having issues with the GetProfileDataAsync
The UserService that was built for v1.6.3 works fine and we can see 12 requested claim types in requestedClaimTypes
public Task<IEnumerable<Claim>> GetProfileDataAsync(ClaimsPrincipal subject,
IEnumerable<string> requestedClaimTypes = null)
{
var userClaims = claimsService.GetByUserIdAsync(int.Parse(subject.GetSubjectId()));
var claims =
userClaims.Where(x => requestedClaimTypes != null && requestedClaimTypes.Contains(x.Type));
return Task.FromResult(claims);
}
But since upgrading to v2.5, the only requested claim type is sub in context.RequestedClaimTypes, rather than 12 we used to get. The only way to get all 12 in is to change the AlwaysIncludeInIdToken to true
Our updated UserService for v2.5 is
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
if (context == null) throw new ArgumentNullException("context");
var subject = context.Subject;
var requestedClaimTypes = context.RequestedClaimTypes;
var userClaims = await _claimsService.GetByUserIdAsync(int.Parse(subject.GetSubjectId()));
if (userClaims != null)
{
var claims = userClaims.Where(x => requestedClaimTypes != null && requestedClaimTypes.Contains(x.Type));
context.IssuedClaims = claims;
}
}
We use SQL to store our Clients and Scopes but we've not changed any data, other than to use the IdentityServer3.EntityFramework provider
Our logging shows that the 4 scopes are being requested which have their associated scope claims as before
Info: Authorize request validation success {
"ClientId": "MyApp",
"ClientName": "MyApp",
"RedirectUri": "https://xxx:44300/",
"AllowedRedirectUris": [
"https://xxx:44300/"
],
"SubjectId": "9",
"ResponseType": "code id_token",
"ResponseMode": "form_post",
"Flow": "Hybrid",
"RequestedScopes": "openid profile roles user",
"State": "OpenIdConnect.AuthenticationProperties=xxxx",
"Nonce": "xxx",
"SessionId": "xxx",
"Raw": {
"client_id": "MyApp",
"redirect_uri": "https://xxx:44300/",
"response_mode": "form_post",
"response_type": "code id_token",
"scope": "openid profile roles user",
"state": "OpenIdConnect.AuthenticationProperties=xxx",
"nonce": "xxx"
}
}
What do we need to do to get it to request all the claim types as before??
The spec says that if an access token is requested, the id_token should only contain the minimal user-related claims (aka sub). The access token can then be used to retrieve the other claims from the userinfo endpoint.
This is an optimization mechanism to keep the id_token as small as possible.
We had a bug where this was done for id_token token but not for code id_token (which is what you are using). This bug was fixed at some point along the way. I guess that is the behavioural change you are seeing.
Either set the AlwaysIncludeInIdToken property on the scope claims you want to be included - or use the userinfo endpoint to retrieve the claims.