7

I have an IdentityServer4 asp.net-core host setup for Resource Owner Password Grant using JWT Bearer tokens and an API in a separate asp.net-core host which has my API and two Angular clients.
The Authentication and Authorization is working from my two Angular clients to my API.
Now I need to expose an API in the IdentityServer4 host so I can create users from one of the Angular clients.
I have copied my Authentication and Authorization setup from my API over to my IdentityServer4 host, however, I cannot get it to Authenticate.

In the below code, within the API, I can set a breakpoint on the jwt.Authority... line and the first call will trigger this breakpoint in my API but not in the IdentityServer4 host.

Authentication

services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
  .AddJwtBearer(jwt =>
  {
    jwt.Authority = config.Authentication.Authority; //Breakpoint here
    jwt.RequireHttpsMetadata = config.Authentication.RequireHttpsMetadata;
    jwt.Audience = Common.Authorization.Settings.ServerApiName;
  });

Authorization

I'm not sure if it's relevant, but I'm using role based authorization, the following is the setup for this.

var authPolicyBuilder = new AuthorizationPolicyBuilder()
    .RequireRole(Common.Authorization.Settings.ServerApiRoleBasePolicyName)
    .Build();

services.AddMvc(options =>
    {
        options.Filters.Add(new AuthorizeFilter(authPolicyBuilder));

        ...

services.AddAuthorization(options =>
    {
        options.AddPolicy(Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName, policy =>
        {
            policy.RequireClaim("role", Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName);

I've extracted the following from my logging: What I see is that in the non-working case, I never get to the point of invoking the JWT validation (#3 in the working logs).
This is just a tiny extract of my logs, I can share them in entirety if needs be.

Working

1 Request starting HTTP/1.1 GET http://localhost:5100/packages/
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
2 Connection id "0HLC8PLQH2NRU" started.
(SourceContext:Microsoft.AspNetCore.Server.Kestrel)
3 Request starting HTTP/1.1 GET http://localhost:5000/.well-known/openid-configuration
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
--Truncated--

Not Working

1 Request starting HTTP/1.1 GET http://localhost:5000/users
(SourceContext:Microsoft.AspNetCore.Hosting.Internal.WebHost)
--Truncated--

Clients

new Client
{
    ClientId = "setup_app",
    ClientName = "Setup App",
    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
    AccessTokenType = AccessTokenType.Jwt,
    AccessTokenLifetime = 3600,
    IdentityTokenLifetime = 3600,
    UpdateAccessTokenClaimsOnRefresh = true,
    SlidingRefreshTokenLifetime = 3600,
    AllowOfflineAccess = false,
    RefreshTokenExpiration = TokenExpiration.Absolute,
    RefreshTokenUsage = TokenUsage.OneTimeOnly,
    AlwaysSendClientClaims = true,
    Enabled = true,
    RequireConsent = false,
    AlwaysIncludeUserClaimsInIdToken = true,

    AllowedCorsOrigins = { config.CorsOriginSetupClient },


    ClientSecrets =
    {
        new Secret(Common.Authorization.Settings.ServerApiSetupClientSecret.Sha256())
    },

    AllowedScopes =
    {
        Common.Authorization.Settings.ServerApiName,
    }
},
new Client
{
    ClientId = "client_app",
    ClientName = "Client App",
    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
    AccessTokenType = AccessTokenType.Jwt,
    AccessTokenLifetime = 3600,
    IdentityTokenLifetime = 3600,
    UpdateAccessTokenClaimsOnRefresh = true,
    SlidingRefreshTokenLifetime = 3600,
    AllowOfflineAccess = false,
    RefreshTokenExpiration = TokenExpiration.Absolute,
    RefreshTokenUsage = TokenUsage.OneTimeOnly,
    AlwaysSendClientClaims = true,
    Enabled = true,
    RequireConsent = false,
    AlwaysIncludeUserClaimsInIdToken = true,

    AllowedCorsOrigins = { config.CorsOriginSetupClient },


    ClientSecrets =
    {
        new Secret(Common.Authorization.Settings.ServerApiAppClientSecret.Sha256())
    },

    AllowedScopes =
    {
        Common.Authorization.Settings.ServerApiName,
    }
}

IdentityResources

return new List<IdentityResource>
{
    new IdentityResources.OpenId(),
    new IdentityResources.Profile(),
    new IdentityResource(Common.Authorization.Settings.ServerApiScopeName, new []{
        "role",
        Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName,
        Common.Authorization.Settings.ServerApiAppClientAdminRolePolicyName,
        Common.Authorization.Settings.ServerApiAppClientUserRolePolicyName,
}),
};

User

var adminUser = new ApplicationUser
{
    UserName = "admin",
    Email = "admin@noreply",
};
adminUser.Claims = new List<IdentityUserClaim>
{
    new IdentityUserClaim(new Claim(JwtClaimTypes.PreferredUserName, adminUser.UserName)),
    new IdentityUserClaim(new Claim(JwtClaimTypes.Email, adminUser.Email)),
    new IdentityUserClaim(new Claim("role", Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName)),
    new IdentityUserClaim(new Claim("role", Common.Authorization.Settings.ServerApiRoleBasePolicyName)),
    new IdentityUserClaim(new Claim("profileImage", $"https://robohash.org/{Convert.ToBase64String(System.Security.Cryptography.MD5.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(adminUser.UserName)))}?set=set2"))
};
adminUser.AddRole(Common.Authorization.Settings.ServerApiSetupClientAdminRolePolicyName);

API

new ApiResource(Common.Authorization.Settings.ServerApiName, "Server API"){
    ApiSecrets =
    {
        new Secret(Common.Authorization.Settings.ServerApiAppClientSecret.Sha256())
    },
}, 
John B
  • 1,129
  • 14
  • 23
  • Did I understand you correctly that you want to send api calls to the same server that is also hosting the identity server? Please provide code about your identityserver confguration for api resources, client resources and so on. – alsami Mar 13 '18 at 11:14
  • Correct, both the identity host and api host are on the same machine (local dev) currently. I'll add code for identity config. Thanks. – John B Mar 13 '18 at 20:52

3 Answers3

6

Look up here https://github.com/IdentityServer/IdentityServer4.Samples

Seems like it should be like:

Authentication:

services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = config.Authentication.Authority;

                options.RequireHttpsMetadata = false;

                options.ApiName = ServerApiName;
                options.ApiSecret = ServerApiAppClientSecret;
            });

Or with JWT you can try like:

services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(options =>
        {
            options.Authority = config.Authentication.Authority;
            options.RequireHttpsMetadata = false;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = true,
                ValidAudiences = new[]
                {
                    $"{config.Authentication.Authority}/resources",
                    ServerApiName
                },
            };
        });

Also, you will able to add authorization policy, like:

Authorization:

services.AddMvc(opt =>
            {
                var policy = new AuthorizationPolicyBuilder()
                       .RequireAuthenticatedUser()
                       .RequireScope("api").Build();
                opt.Filters.Add(new AuthorizeFilter(policy));
            })
Oleksandr Nahirniak
  • 1,357
  • 2
  • 9
  • 12
  • 3
    Thanks Oleksandr, this worked. I modified my JWT Auth setup to be like you wrote and it now works. I think that setting the options for DefaultAuthenticationScheme and DefaultChallengeScheme to "Bearer" instead of just passing "Bearer" as the defaultScheme to AddAuthentication fixed it as if I add those 2 lines and leave the rest as is, it also works. In my logs on startup, I now see "Using Bearer as default scheme for challenge / forbid" instead of "Using Identity.Application as default scheme for challenge / forbid". Thanks for your help, much appreciated. (Sorry about the double comments) – John B Mar 13 '18 at 22:52
  • Same for me !! Replacing the one-line `AddAuthentication` with the full config worked ! Thanks :) – Christophe Gigax Jan 15 '21 at 15:51
2

There is a MS out of the box service designed to add support for local APIs

First in IDS4 startup.cs, ConfigureServices add

services.AddLocalApiAuthentication();

Then in IDS4 config.cs in your client declaration

AllowedScopes = {
     IdentityServerConstants.LocalApi.ScopeName,  <<<< ---- This is for IDS4 Api access
     IdentityServerConstants.StandardScopes.OpenId,
     IdentityServerConstants.StandardScopes.Profile,
     IdentityServerConstants.StandardScopes.Email
     ...

Still in config add the new scope to ApiScopes

new ApiScope(IdentityServerConstants.LocalApi.ScopeName)

Note

IdentityServerConstants.LocalApi.ScopeName resolves to 'IdentityServerApi'

Now in your shiny new Api located in the IDS4 project add the authorize tag to your api endpoint

[Authorize(IdentityServerConstants.LocalApi.PolicyName)]
[HttpGet]
public IEnumerable<string> Get()
{
    return new string[] { "value1", "value2" };
}

Finally you need to request this new scope in your Client

Scope = "openid sig1 api1 profile email offline_access company IdentityServerApi",

Thats it

tinmac
  • 2,357
  • 3
  • 25
  • 41
0

I think this could help, it's included in the community samples.

Youssef SABIH
  • 629
  • 6
  • 11