I am implementing Identity Server in a razor page application.
When requesting the speech ApiResource, identityserver returns "invalid_scope". My understanding is that the resource is a group of scopes. So, I was expecting the identityserver to return the scopes defined in the speech resource.
Note: Which I add speech as ApiScope it works fine but then it does not add the speech.synthesize and payment.subscription scopes.
Here's how I have defined the ApiScopes:
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("speech.synthesize", "Speech synthesis",new []{"api.create" }),
new ApiScope("payment.subscription", "Subscription service"),
new ApiScope("payment.manage", "Manage Payment"),
};
And here's how I have defined the ApiResource:
public static IEnumerable<ApiResource> ApiResources =>
new List<ApiResource>
{
new ApiResource("speech", "Speech API")
{
Scopes = { "speech.synthesize", "payment.subscription" }
}
};
And here's the client configuration:
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
AlwaysSendClientClaims = true,
// scopes that client has access to
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"speech"
}
}
};
What is wrong here? Can anybody help me understand the problem.
What is the role of the Api Resource if not grouping the scopes.
You as a client asks for ApiScopes, not ApiResources. One more more ApiResource can point to an ApiScope.
An ApiResource represents an API instance, not a Scope. ApiResources are like clients, but for Apis.
See my answer here for more details about the difference between IdentityResource, ApiResource and ApiScope
Related
I have an ASP.NET Core Web API with Swagger integrated using Swashbuckle. I have successfully integrated authorization on Swagger UI using an operation filter, because I do not want to show padlock for anonymous APIs.
.OperationFilter<AuthorizeFilter>()
Inside the filter, I have registered basic auth security requirement for Swagger UI.
My problem is, even though authentication is happening in APIs on Swagger UI, I no longer see that nice authentication popup which is giving when click on the padlock icon.
Could someone answer, why I am not seeing the auth popup now?
Assuming you have some endpoints that protected with [Authorize] attribute (can also be put on the controller).
[Route("")]
public class HelloController : ControllerBase
{
[Authorize]
[HttpGet("secure")]
public IActionResult GetSomethingPrivate()
{
return Ok("secret");
}
[HttpGet("public")]
public IActionResult GetSomethingPublic()
{
return Ok("hey");
}
}
You need to define a security scheme suitable for your needs. But do not require it globally, instead add it inside an operation filter. Here I've added a simple token auth:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "ApiPlayground", Version = "v1" });
c.AddSecurityDefinition("token", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
Name = HeaderNames.Authorization,
Scheme = "Bearer"
});
// dont add global security requirement
// c.AddSecurityRequirement(/*...*/);
c.OperationFilter<SecureEndpointAuthRequirementFilter>();
});
}
And here's the operation filter which references the token auth scheme we've just created. It checks if the endpoint needs authentication, then adds the requirement.
internal class SecureEndpointAuthRequirementFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!context.ApiDescription
.ActionDescriptor
.EndpointMetadata
.OfType<AuthorizeAttribute>()
.Any())
{
return;
}
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "token" }
}] = new List<string>()
}
};
}
}
When you run the app, it works as you expect:
So does the auth popup:
Bonus: using basic auth
Define a new security scheme with following values:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddSwaggerGen(c =>
{
// ...
// basic auth scheme (username + password)
c.AddSecurityDefinition("basic", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "basic"
});
// dont add global security requirement
// c.AddSecurityRequirement(/*...*/);
c.OperationFilter<SecureEndpointAuthRequirementFilter>();
});
}
Then update the operation filter to reference basic auth scheme:
internal class SecureEndpointAuthRequirementFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!context.ApiDescription
.ActionDescriptor
.EndpointMetadata
.OfType<AuthorizeAttribute>()
.Any())
{
return;
}
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "basic" // <-- changed "token" -> "basic"
}
}] = new List<string>()
}
};
}
}
here's how the auth popup looks:
After logging in, requests include the correct Authorization header.
In my case, I am using JWT Token Authentication with .NET Core API. I Configure the swagger with the authorization token using the below code. This code will add global security requirements.
In Startup Class ConfigureServices Method.
//Swagger Configuration
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "API",
Version = "v2",
Description = "Your Api Description"
});
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme (Example: 'Bearer 12345abcdef')",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
And In Configure Method
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "API");
});
After running the API project Authorize button will appear on the right side. On Click the authorize button Authorization popup open and then pass the token in the text box with 'Bearer token'.
Authorization working fine for me.
the Abdusco's answer is true but modify the Apply method like this for AllowAnonymousAttribute Methods in Authorized Controllers
if (!context.ApiDescription
.ActionDescriptor
.EndpointMetadata
.OfType<AuthorizeAttribute>()
.Any() || context.ApiDescription
.ActionDescriptor
.EndpointMetadata
.OfType<AllowAnonymousAttribute>()
.Any())
{
return;
}
I have been searching for how IdentityServer4 uses DB. I have read: https://docs.identityserver.io/en/release/quickstarts/8_entity_framework.html and looked at the QuickStart4 which uses a DB store. What I can't find is how I can use it in a many clients scenario where we want to add client details to DB only without having to add the client to the config.cs in Identity Server like so:
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("api1", "My API")
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
// machine to machine client
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
// scopes that client has access to
AllowedScopes = { "api1" }
},
// interactive ASP.NET Core MVC client
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// where to redirect to after login
RedirectUris = { "https://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
};
}
}
see this page Entity Framework Integration and this page Entity Framework Support
Basically, what you need to do is:
Add this NuGet package IdentityServer4.EntityFramework
Apply the migrations to create the necessary tables or use the pre-made SQL scripts here
Add the AddConfigurationStore to your startup class
Alternatively, you implement your own IClientStore implementation.
I have a typical Blazor WASM project, Server, Client and Shared. Authentication with IdentityServer is setup and working correctly. I'm receiving the JWT when I login with a user and I can get the discovery document.
Aside from normal users, I want to connect devices. These devices are not users so it seems wrong to create a user for each device. If I create a user then I can login and get the token. But I added InMemoryClients, so I was under the assumption that I could login as a client with id/secret using the device authentication endpoint of the discovery document.
I added a console application that is running on the device. But authentication fails returning "invalid_client".
In the server app I defined a Client and ApiScope:
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("api", "My API")
};
public static IEnumerable<IdentityServer4.Models.Client> Clients =>
new List<IdentityServer4.Models.Client>
{
new IdentityServer4.Models.Client
{
ClientId = "device",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret")
},
// scopes that client has access to
AllowedScopes = { "api" }
}
};
They are added with the AddInMemory functions in the Startup.Configure method:
services.AddIdentityServer()
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.IdentityResources["openid"].UserClaims.Add("name");
options.ApiResources.Single().UserClaims.Add("name");
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("role");
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:44316";
options.Audience = "api";
})
.AddIdentityServerJwt();
In the console app I send a DeviceAuthenticationRequest:
static IDiscoveryCache _cache = new DiscoveryCache("https://localhost:44316");
var disco = await _cache.GetAsync();
if (disco.IsError) throw new Exception(disco.Error);
var client = new HttpClient();
response = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest
{
//Address = disco.TokenEndPoint, // same result
Address = disco.DeviceAuthorizationEndpoint,
ClientId = "device",
ClientSecret = "secret",
Scope = "api"
});
The response in the client contains an error, "invalid_client". And the server complains also:
IdentityServer4.Validation.ClientSecretValidator: Error: No client with id 'client' found. aborting
I hope my question is clear, thanks in advance.
EDIT
Obviously the typo was a problem. But I managed to fix it by changing the AllowedGrantTypes to:
AllowedGrantTypes = { GrantTypes.ClientCredentials, GrantTypes.DeviceFlow };
However! I had to removed api authorization, need to figure out how to add it back:
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(options =>
{
options.IdentityResources["openid"].UserClaims.Add("name");
options.ApiResources.Single().UserClaims.Add("name");
options.IdentityResources["openid"].UserClaims.Add("role");
options.ApiResources.Single().UserClaims.Add("role");
});
And instead of specifying options for and adding JwtBearer:
//services.AddAuthentication("Bearer")
// .AddJwtBearer("Bearer", options =>
// {
// options.Authority = "https://localhost:44316";
// options.Audience = "api";
// })
// .AddIdentityServerJwt();
I just have this:
services.AddAuthentication()
.AddIdentityServerJwt();
I think you have just a small typo. In the Startup.cs the ClientId = "device". In your client, you set ClientId = "client".
In general, you are right. You don't need to create a client for each user and their potential devices. The definition of one client for all possible users and devices is enough. You could even debate if a client's password is necessary and increase security. In the official docs, the secret is optional.
In a simplified view: the "client part" of this authentication flow is mainly used to generate the user code. In a second (asynchronous) step, a user with different device logins to authenticate the device by entering the previously generated code.
For more details of a look at this post.
I have a project done with Asp.Net Core 2.0 API, Identity Server and WPF app.
I am able to access the API from WPF after I login in.
Now I am trying to implement roles so I can be able to authorize just certain users to access the API.
In Config.cs I am declaring my Client and add to the scope :
new Client
{
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.OfflineAccess,
"fiver_auth_api",
"role"
},
AlwaysIncludeUserClaimsInIdToken=true
}
Declaring TestUser:
return new List<TestUser>
{
new TestUser
{
SubjectId = "", Username = "", Password = "",
Claims = new List<Claim>
{
new Claim(JwtClaimTypes.Email, "AliceSmith#email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.Role, "Admin"),
new Claim(JwtClaimTypes.Scope, "openid offline_access fiver_auth_api")
}
}
}
And in the controller I am using :
[Authorize(Roles = "Admin")]
Why I don`t get the user claims in the token?
For who is interested there is how I fixed it:
In your configuration file add a list for your roles:
new ApiResource
(
"fiver_auth_api",
"Fiver.Security.AuthServer.Api",
new List<string> {"role"} <--- Add this line to your API
)
I have set-up identityserver3 and MVC4 client using this tutorial. When I configured client to use 'Implicit' flow things are working as expected and I am getting back 'profile' scope. i.e. I can find claims first_name and given_name. Below my configuration code.
Client and User configuration
public static class Users
{
public static List<InMemoryUser> Get()
{
return new List<InMemoryUser>
{
new InMemoryUser
{
Username = "Bob",Password = "password",Subject = "1",
Claims = new []
{
new Claim(Constants.ClaimTypes.GivenName,"firstName"),
new Claim(Constants.ClaimTypes.FamilyName,"lastName")
}
}
};
}
}
public static class Clients
{
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientId = "MVC",
ClientName = "MVC Client Name",
RedirectUris = new List<string>
{
"https://localhost:44302/"
},
Flow = Flows.Implicit,
AllowAccessToAllScopes = true
}
};
}
}
Identity Server Configuration
public void Configuration(IAppBuilder app)
{
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.Map("/identity", appBuilder => {
appBuilder.UseIdentityServer(new IdentityServer3.Core.Configuration.IdentityServerOptions
{
SiteName = "Site Name",
SigningCertificate = LoadCertificate(),
RequireSsl = false,
Factory = new IdentityServer3.Core.Configuration.IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryUsers(Users.Get())
.UseInMemoryScopes(StandardScopes.All)
});
});
app.UseCookieAuthentication(new Microsoft.Owin.Security.Cookies.CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44302/identity",
ClientId = "MVC",
RedirectUri = "https://localhost:44302/",
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
Scope = "openid profile"
});
}
In my MVC application I have secured Action on Home controller named 'Contact'
[Authorize]
public ActionResult Contact()
{
ClaimsPrincipal principal = User as ClaimsPrincipal;
return View(principal.Claims);
}
And finally here is simple view
#model IEnumerable<System.Security.Claims.Claim>
#foreach (var item in Model)
{
<div>
<span>#item.Type</span>
<span>#item.Value</span>
</div>
}
</div>
Now when I run this app, after clicking on secure 'Contact' link I am being redirected to STS server and after providing credentials I can see below output.
Note that claims given_name and family_name exists in the claims returned by STS.
Problem:
The moment I change Client to support Hybrid flow. I am not getting back claims given_name and family_name
I made below changes to my code.
Client configuration
public static IEnumerable<Client> Get()
{
return new[]
{
new Client
{
ClientId = "MVC",
ClientName = "MVC Client Name",
RedirectUris = new List<string>
{
"https://localhost:44302/"
},
Flow = Flows.Hybrid,//Changed this to Hybrid
AllowAccessToAllScopes = true
}
};
}
Server Configuration
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44302/identity",
ClientId = "MVC",
RedirectUri = "https://localhost:44302/",
ResponseType = "code id_token token", //Changed response type
SignInAsAuthenticationType = "Cookies",
Scope = "openid profile"
});
After running applicaton I can see below claims returned by STS
Note that claims given_name and family_name are missing this time.
Have I missed anything?
When you only ask for an id_token all the claims for the user are in the id_token. When you change your request to get a token (either by asking for code or token) then only the user claims configured as "AlwaysInclude" are included in the id_token. The rest must be retrieved from the user info endpoint using the access_token you received. You can use the helper APIs in the IdentityModel library to easily access the user info endpoint. Our samples show how you can do this: https://github.com/IdentityServer/IdentityServer3.Samples/blob/master/source/Clients/MVC%20OWIN%20Client%20(Hybrid)/Startup.cs#L66