Identityserver4 and API in single project - c#

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())
},
},

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));
})

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

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

Related

How do I allow specific role to access the endpoint?

Trying to restrict access to an endpoint only for users with the role "Manager".
when I open swagger, I use Login endpoint to login(it's successful) and then I try to run 'AddCategory' endpoint and response is 404.
[Authorize(Roles = "Manager")]
[HttpPost("addCategory")]
[SwaggerResponse((int)HttpStatusCode.OK, type: null)]
[SwaggerResponse((int)HttpStatusCode.Unauthorized)]
[SwaggerResponse((int)HttpStatusCode.Forbidden)]
[SwaggerResponse((int)HttpStatusCode.InternalServerError)]
public Category AddCategory([FromBody] Category category)
{
context.Categories.Add(category);
context.SaveChanges();
return category;
}
Same happens if I do not log in. If I remove Authorize attribute, everything works fine.
I checked UserRoles table and user I'm logging in with is Manager. Also tried to put only [Authorize] attribute instead of role specific and still didn't work. In Swagger, Login endpoint returns 200 response but is there any other way to check if I'm logged in or not?
EDIT
This is how I Generate JwtToken on Login.
var roleId = _appDbContext.UserRoles.Where(x => x.UserId == user.Id)
.Select(x=>x.RoleId)
.FirstOrDefault();
var role = _appDbContext.Roles.Where(x=>x.Id==roleId).FirstOrDefault();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.Role, role.Name)
}),
Expires = DateTime.UtcNow.AddSeconds(30), // 5-10
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
Decoded token looks like this
"alg": "HS256",
"typ": "JWT"
}.{
"Id": "3f4b1e8f-d15c-426e-9aa6-375ac53f27ae",
"email": "manager#example.com",
"sub": "manager#example.com",
"jti": "6e1fb89e-5408-4c61-8258-8d2c7cead4ab",
"role": "Manager",
"nbf": 1658758635,
"exp": 1658758665,
"iat": 1658758635
}.[Signature]
However, I've never added anything in [AspNetRoleClaims] table. Should I add data to that table too?
EDIT :
This is Program.cs
var key = Encoding.ASCII.GetBytes("llvudfvkwvepwkdnsnwmuulyvtrawppf");
var tokenValidationParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
RequireExpirationTime = false,
ClockSkew = TimeSpan.Zero
};
builder.Services.AddSingleton(tokenValidationParams);
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt =>
{
jwt.SaveToken = true;
jwt.TokenValidationParameters = tokenValidationParams;
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Add app.UseAuthentication(); before app.UseAuthorization(); in program.cs file

Infinite redirects between Identity Server and Website

I've got the following client set up in IdentityServer:
internal class Clients
{
public static IEnumerable<Client> Get()
{
return new List<Client>
{
new Client
{
ClientId = "Client",
ClientName = "Client",
ClientSecrets = new List<Secret> {new Secret("blablabla".Sha256())},
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { Startup.Configuration.GetValue<string>("Clients", "https://localhost:5002") + "/signin-oidc" },
PostLogoutRedirectUris = { Startup.Configuration.GetValue<string>("Clients", "https://localhost:5002") + "/signout-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"roles",
"Business-to-Business-users",
"reporting-urls"
},
RequirePkce = true,
AllowPlainTextPkce = false,
AllowAccessTokensViaBrowser = true
}
};
}
}
I'm trying to modify the mvc client to login to the above identity server. In startup.cs I modified cllientID, ClientSecret and Authority to match the client settings. This is my ConfigureServices():
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie("cookie")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = Configuration.GetValue<string>("IdentityHost", "https://localhost:5000");
options.ClientId = "Client";
options.ClientSecret = "blablabla";
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.ResponseType = "code";
options.UsePkce = true;
options.ResponseMode = "query";
options.Scope.Add("roles");
options.Scope.Add("Business-to-Business-users");
options.Scope.Add("reporting-urls");
options.RequireHttpsMetadata = false;
options.ClaimActions.MapUniqueJsonKey("role", "role");
options.ClaimActions.MapUniqueJsonKey("Business-to-Business-user", "Business-to-Business-user");
options.ClaimActions.MapUniqueJsonKey("reporting-url", "reporting-url");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = "role",
};
});
first I got a bug that he needs https but instead uses http. So I added the following code to my Configure:
app.Use((context, next) => {
context.Request.Scheme = "https";
return next();
});
Now that this error was solved, I ran into a different one.
As soon as I'm trying to login to my client, I got the following error in my browser (Opera): "ERR_TOO_MANY_REDIRECTS".
Does anyone have an idea how I can proceed? Both Identity and the client are seperate docker containers.

Can't send logout request to Identity Server from MVC application

in my application I accept owin authentication towards Identity Server. Everything works well, except the logout part. In practice when I want to logout I expect to be logged out also from my Identity server but this doesn't happen and I logout only from my application. The code is the following:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Login.aspx")
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
var AuthorityUrl = ConfigurationManager.AppSettings["AuthorityUrl"];
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = AuthorityUrl,
RedirectUri = $"{ConfigurationManager.AppSettings["PortalWebUrl"]}/signin-oidc",
PostLogoutRedirectUri = $"{ConfigurationManager.AppSettings["PortalWebUrl"]}/signout-callback-oidc",
RequireHttpsMetadata = false,
ClientId = "portal-local",
AuthenticationType = "oidc",
SignInAsAuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
ResponseType = "id_token token",
Scope = "openid profile email",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
var claimsToExclude = new[]
{
"aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
};
var claimsToKeep = n.AuthenticationTicket.Identity.Claims.Where(x => !claimsToExclude.Contains(x.Type)).ToList();
claimsToKeep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
var ci = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
ci.AddClaims(claimsToKeep);
n.AuthenticationTicket = new Microsoft.Owin.Security.AuthenticationTicket(ci, n.AuthenticationTicket.Properties);
return Task.CompletedTask;
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
n.ProtocolMessage.IdTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token")?.Value;
}
return Task.CompletedTask;
}
}
});
the "warning" coming back from Identity Server is the following:
Can you help me figuring out what am I doing wrong?

Identity Server 4 and external provider returning plain HTML if unauthenticated

We have a machine running IdentityServer4 which itself is used as a federated gateway to Office 365.
I'm trying to figure out how to circumvent this scenario: we create an MVC-app that has the following owin-startup class:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies",
ExpireTimeSpan = TimeSpan.FromMinutes(1),
SlidingExpiration = false
});
JwtSecurityTokenHandler.InboundClaimTypeMap.Clear();
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "Cookies",
Authority = "https://localhost:5000/",
ClientId = "dev",
RedirectUri = "http://localhost:54509/",
ResponseType = "id_token token",
Scope = "openid profile account",
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var claims_to_exclude = new[]
{
"aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
};
var claims_to_keep =
n.AuthenticationTicket.Identity.Claims
.Where(x => false == claims_to_exclude.Contains(x.Type)).ToList();
claims_to_keep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
var ci = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
"name", "role");
ci.AddClaims(claims_to_keep);
n.AuthenticationTicket = new AuthenticationTicket(
ci, n.AuthenticationTicket.Properties
);
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var id_token = n.OwinContext.Authentication.User.FindFirst("id_token")?.Value;
n.ProtocolMessage.IdTokenHint = id_token;
}
return Task.FromResult(0);
}
}
});
app.UseStageMarker(PipelineStage.Authenticate);
So far so good: upon first visiting the user is redirected to the Identity Server, signing in and getting back but when they lose their token or become unauthenticated and try to access a controller decorated with [Authorize] the plain HTML of the sign-on site is returned to that controller.
Are we using the wrong flow for this back channel, server-to-server communication? Or is there a way to intercept this so that they can be redirected immediately before the controller freaks out over getting HTML instead of JSON.

Identity Server 4 Swagger Authentication

I'm currently having issues using swagger to authorize an api call to identity server 4.
My swagger dependency is using swashbuckle version
-beta client object in the identity server 4 looks like
new Client
{
ClientId="swagger",
Enabled = true,
ClientName="Swagger",
AllowedGrantTypes = GrantTypes.Implicit,
ClientSecrets = new List<Secret>
{
new Secret("secret".Sha256())
},
AllowedScopes = new List<string>
{
"apil"
},
RedirectUris = new List<string>
{
"http://localhost:15138/swagger/ui/popup.html"
},
AllowedCorsOrigins = new List<string>
{
"http://localhost:15138",
"http://localhost:15138"
},
AllowAccessTokensViaBrowser = true,
AllowAccessToAllScopes= true
}
The client object is the identity server 4 model
in the configure method for authentication I have this
app.UseIdentityServer();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:15138/",
ScopeName = "apil",
RequireHttpsMetadata = false,
});
Using fiddler my get request looks like this
GET
/connect/authorize?state=9321480892748389&nonce=5279296493808222&client_id=swagger&redirect_uri=http%3A%2F%2Flocalhost%3A15138%2Fswagger%2Fui%2Fpopup.html&response_type=id_token%20token&scope=apil
HTTP/1.1
All the necessary parameters are there, the client has the corresponding client id, but The response I get back is a redirect to an error page with the message of an invalid request. I was anticipating a login page to pass in credentials or something similar to get authorized I was wondering what I did wrong for that to happen.
I ran into this same problem, and it was related to a few different things.
Swagger needs a Security Definition configured.
IdentityServerAuthentication AutomaticAuthenticate needs to be true.
Swagger's client-id and client name need to be configured in Startup.cs.
See below:
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new Info
{
Version = "v1",
Title = "my api title",
Description = "my api desc",
TermsOfService = "None",
Contact = new Contact { Name = "contact" }
});
var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "api.xml");
c.IncludeXmlComments(filePath);
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = "https://url",
Scopes = new Dictionary<string, string>
{
{ "api-name", "my api" }
}
});
});
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseIdentity();
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = "https://url",
RequireHttpsMetadata = true,
ApiName = "api-name",
ApiSecret = "api-secret",
AllowedScopes = { "api-name", "openid", "email", "profile" },
ClaimsIssuer = "https://url",
AutomaticAuthenticate = true,
});
app.UseStaticFiles();
app.UseMvc();
// Enable middleware to serve generated Swagger as a JSON endpoint
app.UseSwagger();
// Enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.)
app.UseSwaggerUi(c =>
{
c.ConfigureOAuth2("swagger-name", "swagger-secret", "swagger-realm", "Swagger");
});
}

Categories

Resources