IdentityServer4 read client claims from appsettings.json - asp.net-core

how can I read client claims from the appsettings.json file?
I have this appsettings.json:
"IdentityServer": {
"Clients": [
{
"Enabled": true,
"ClientId": "client1",
"AlwaysSendClientClaims": true,
"Claims": [
{
"Type": "custom_claim1",
"Value": "value1"
},
{
"Type": "custom_claim2",
"Value": "value2"
}
]
}
]
}
And, I load the clients config like the docs says:
var builder = services.AddIdentityServer(opts =>
{
/// Opts removed for simplicity
})
.AddInMemoryClients(Configuration.GetSection("IdentityServer:Clients"));
All is working fine, except for the client claims. I can not see them in Jwt.io decode tool.

There is a problem binding the Claims collection in the Clients[] from appSettings.json due to the fact that the current implementation of the Claim object can not be deserialized from json.
https://github.com/IdentityServer/IdentityServer4/issues/2573
and here
https://github.com/IdentityServer/IdentityServer4/pull/3887/files/ed14abc204960b2d5ca3418a868882a698e54d90

Related

Is it possible to call lambda function from other lambda functions in AWS serverless API?

I am creating a serverless API using AWS SAM template and ASP.Net Core.
I wanted to know if it is possible to call a common lamnda function from multiple lambda functions?
I have 2 APIs for user authentication.
/user/authenticate
/admin/authenticate
Now when the user calls these API endpoints I want to call a common lambda function which will look like following:
public AuthResponse Authenticate(AuthInfo info, int role);
I get a user role based on which API endpoint is called. For example if /user/authetication is called then role=1 otherwise role=0.
And then I want Authenticate() lambda to perform user authentication based on the AuthInfo + Role.
I want to do this because all my users are stored in the same table and I would like to cross verify if user has the correct role to access the feature.
I will also share a portion of serverless.template used for above APIs.
/admin/authenticate
"Handler": "Server::API.Admin::Authenticate",
"Description" : "Allows admin to authenticate",
"Runtime": "dotnetcore2.1",
"CodeUri": "",
"MemorySize": 256,
"Timeout" : 300,
"Role": {"Fn::GetAtt" : [ "LambdaExecutionRole", "Arn"]},
"FunctionName" : "AdminAuthenticate",
"Events":
{
"PutResource":
{
"Type": "Api",
"Properties":
{
"Path": "/v1/admin/authenticate",
"Method": "POST"
}
}
}
}
}
/user/authenticate
"Handler": "Server::API.User::Authenticate",
"Description" : "Allows user to authenticate",
"Runtime": "dotnetcore2.1",
"CodeUri": "",
"MemorySize": 256,
"Timeout" : 300,
"Role": {"Fn::GetAtt" : [ "LambdaExecutionRole", "Arn"]},
"FunctionName" : "UserAuthenticate",
"Events":
{
"PutResource":
{
"Type": "Api",
"Properties":
{
"Path": "/v1/user/authenticate",
"Method": "GET"
}
}
}
}
}
As you can see above, 2 lambda functions are created AdminAuthenticate and UserAuthentication. I want these lambda functions to share the common code.
Does anyone has any idea how to do it?
Thanks and Regards.
I can think about 2 options to achieve your goal. In the first option, you use multiple Lambda functions, one for each endpoint, both pointing to your same codebase. In the second option, you have a single Lambda function that handles all authentication needs.
Single codebase, multiple functions
In this case, you can define your template file with 2 functions but use the CodeUri property to point to the same codebase.
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Resources": {
"AdminFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "Server::API.Admin::Authenticate",
"Description": "Allows admin to authenticate",
"Runtime": "dotnetcore2.1",
"CodeUri": "./codebase_path/",
"MemorySize": 256,
"Timeout": 300,
"FunctionName": "AdminAuthenticate",
"Events": {
"PutResource": {
"Type": "Api",
"Properties": {
"Path": "/v1/admin/authenticate",
"Method": "POST"
}
}
}
}
},
"UserFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "Server::API.User::Authenticate",
"Description": "Allows user to authenticate",
"Runtime": "dotnetcore2.1",
"CodeUri": "./codebase_path/",
"MemorySize": 256,
"Timeout": 300,
"FunctionName": "UserAuthenticate",
"Events": {
"PutResource": {
"Type": "Api",
"Properties": {
"Path": "/v1/user/authenticate",
"Method": "POST"
}
}
}
}
}
}
}
Single codebase, single function
In this case, you will expose 2 endpoints on API Gateway, but they will be directed to the same handler on your function. Therefore, you will need to write some logic on your code to handle the login properly. The event object passed to your Lambda function will have information on the original URL in the path property (reference -- even though this is for Lambda proxy, still applies).
The template file in this case would be similar to (note I replaced Admin/User terms to "Any", since this will handle any form of authentication):
{
"AWSTemplateFormatVersion": "2010-09-09",
"Transform": "AWS::Serverless-2016-10-31",
"Resources": {
"AnyFunction": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Handler": "Server::API.Any::Authenticate",
"Description": "Allows any to authenticate",
"Runtime": "dotnetcore2.1",
"CodeUri": "./hello_world/",
"MemorySize": 256,
"Timeout": 300,
"Events": {
"UserEndpoint": {
"Type": "Api",
"Properties": {
"Path": "/v1/user/authenticate",
"Method": "POST"
}
},
"AdminEndpoint": {
"Type": "Api",
"Properties": {
"Path": "/v1/admin/authenticate",
"Method": "POST"
}
}
}
}
}
}
}
You can invoke lambda functions through any lambda function, using aws sdk in your chosen language and there you have invoke function defined.
For a reference here is the link to boto3 invoke definition.
Also
The approach you are using for authentication using common codebase is not the right one.
If you need a lambda function to check or authentication particular request, you can setup custom authorizer for that, which in your terms i can say, share lambda code or call it first before invoking the lambda you setup for the particular endpoint with the possibility to pass the custom data if you want.
A Lambda authorizer (or as a custom authorizer) is an API Gateway feature that uses a Lambda function to control access to your API.
If still this doesn't solve your problem and you want common codebase, you can point out as many as api endpoint's to same lambda function.
Then you have to handle event['resources'] inside your codebase.
This is not the recommended way, but you can use it.
You can refer aws samples to setup custom authorizer or the documentation is fair enough to clear all your doubts.

IdentityServer4 {"error":"invalid_client"}

I am using IdentityServer4 (version 3.0.2.0) and facing no client id issue. The exact error is
ERROR| No client with id 'myclientId' found. aborting
Startup.cs of IdentityServer4 project
services.AddIdentityServer()
.AddDeveloperSigningCredential()
// .AddInMemoryCaching()
.AddInMemoryApiResources(Configuration.GetSection("IdentityServer:ApiResources"))
.AddInMemoryClients(Configuration.GetSection("IdentityServer:Clients"))
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString,
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = Convert.ToBoolean(Configuration["CleanUp:IsEnabled"]);
options.TokenCleanupInterval = Convert.ToInt32(Configuration["CleanUp:Interval"]); // interval in seconds
});
Also, I have sha256 converted client_secret in appsettings.json file, sample appsettings.json
"IdentityServer": {
"ApiResources": [
{
"Name": "myapi",
"DisplayName": "my api",
"Scopes": [
{
"Name": "mycustomscope"
},
{
"Name": "openid"
}
],
"ApiSecrets": [
{
"Value": "my sha256 converted secret string",
"Description": "my api"
}
]
}
],
"Clients": [
{
"Enabled": true,
"ClientId": "myclientId",
"AccessTokenLifetime": 100000000,
"ProtocolType": "oidc",
"RequireClientSecret": true,
"IdentityTokenLifetime": 300,
"AuthorizationCodeLifetime": 300,
"ConsentLifetime": 300,
"AbsoluteRefreshTokenLifetime": 2592000,
"SlidingRefreshTokenLifetime": 1296000,
"RefreshTokenExpiration": true,
"AlwaysSendClientClaims": false,
"ClientName": "myclientId",
"ClientSecrets": [
{
"Value": "my sha256 converted secret string",
"Type": "SharedSecret"
}
],
"AllowedGrantTypes": [ "client_credentials", "password" ],
"AllowedScopes": [ "mycustomscope", "openid" ],
"RequireConsent": true
}
]
}
Sample token request from postman/JMeter
url: https://myip:port/myappPool/connect/token
method type: POST
Parameters are:
{
"client_id":"myclientId",
"client_secret": "plaintext secret",
"username":"abcdefghijkl",
"scope":"mycustomscope",
"device_id":"custom property",
"password": "mypassword",
"grant_type":"password",
"app_version":"custom property",
"hashed_value":"custom property"
}
I am posting answer to my own question because I have solved the issue. For me below given field was making an issue. After removing this field, the code ran just fine.
"RefreshTokenExpiration": true
Turns out, IdentityServer4.Models.Client does not have any boolean field named RefreshTokenExpiration but class object.

Identity Server 4 PersistedGrant Not Saving Custom Claims

I'm using Identity Server 4 with token based authorization. I haven't implemented profiles at this stage, but for now the Identity Server is working with my custom ResourceOwnerPasswordValidator and is correctly persisting the authorization grants in the PersistedGrants table in my db.
The problem is that the db is failing to also save the custom claims I am applying at this stage.
To create the grant, I am doing the following in my password validator:
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
...
// validate the user using the context, then generate custom claims
var claims = new List<Claim> { new Claim() }; // add claims here
context.Result = new GrantValidationResult(user.Id.ToString(), "password", claims);
return Task.CompletedTask;
}
but when the persisted claim is created in the db, the Data column only contains the default claims:
{
"CreationTime": "2019-08-21T16:38:18Z",
"Lifetime": 2592000,
"AccessToken": {
// audiences, issuer, creation time, etc...
"Claims": [
{
"Type": "client_id",
"Value": "myClient",
"ValueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"Type": "scope",
"Value": "myAPI",
"ValueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"Type": "scope",
"Value": "offline_access",
"ValueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"Type": "sub",
"Value": "16",
"ValueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"Type": "auth_time",
"Value": "1566405497",
"ValueType": "http://www.w3.org/2001/XMLSchema#integer"
}, {
"Type": "idp",
"Value": "local",
"ValueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"Type": "amr",
"Value": "password",
"ValueType": "http://www.w3.org/2001/XMLSchema#string"
}, {
"Type": "name",
"Value": "email#domain.com",
"ValueType": "http://www.w3.org/2001/XMLSchema#string"
}
],
},
}
I feel like the solution is overloading the grants class to be sure the data property is properly initialized, but am new to IDServer4 and don't know how to do this.
I ended up implementing my own IProfileService and providing it as a transient resource in my ID4 startup. The GetProfileDataAsync function implemented by the interface allows you to add claims to the user profile that you want accessible as claims on the authorization side.
I still think claims added in the password validator should be passed without additional code. Requiring an additional profile service to add them twice seems redundant.

Auth.currentAuthenticatedUser not loading name and family name attributes (and others) from Cognito

I'm using the Auth.currentAuthenticatedUser method to retrieve the attributes recorded for the logged user from AWS Cognito - but only basic atributes are showing. The ones I want are "name" and "family name", but they don't seem to be loaded in the Promise.
This is only the beggining, but I'm concerned as I will want to retrieve other attributes which are not showing up, like user picture, for instance.
Tried to use currentAuthenticatedUser and currentUserInfo with the same results.
async componentDidMount() {
await Auth.currentAuthenticatedUser({bypassCache: true})
.then ( user => this.setUserInfo( user ) )
.catch( err => console.log(err))
}
CognitoUser {
"Session": null,
"attributes": Object {
"email": "r...#gmail.com",
"email_verified": true,
"phone_number": "+5...",
"phone_number_verified": false,
"sub": "246e9...",
},
"authenticationFlowType": "USER_SRP_AUTH",
"client": Client {
"endpoint": "https://cognito-idp.us-east-2.amazonaws.com/",
"userAgent": "aws-amplify/0.1.x react-native",
},
"deviceKey": undefined,
"keyPrefix": "CognitoIdentityServiceProvider.12ddetjn0c0jo0npi6lrec63a7",
"pool": CognitoUserPool {
"advancedSecurityDataCollectionFlag": true,
"client": Client {
"endpoint": "https://cognito-idp.us-east-2.amazonaws.com/",
"userAgent": "aws-amplify/0.1.x react-native",
},
"clientId": "12ddetjn0c0jo0npi6lrec63a7",
"storage": [Function MemoryStorage],
"userPoolId": "us-east...",
},
"preferredMFA": "NOMFA",
"signInUserSession": CognitoUserSession {
"accessToken": CognitoAccessToken {
"jwtToken": "e...oJPg",
"payload": Object {
"auth_time": 1565137817,
"client_id": "1...6lrec63a7",
"event_id": "c3...-4bd9-ad42-200f95f9921c",
"exp": 15...2,
"iat": 156...5872,
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-...",
"jti": "5483e...544149c42e58",
"scope": "aws.cognito.signin.user.admin",
"sub": "246e93...f4d8e6f4725b",
"token_use": "access",
"username": "r...f",
},
},
"clockDrift": -2,
"idToken": CognitoIdToken {
"jwtToken": "eyJraWQiOiJk...",
"payload": Object {
"aud": "12ddetjn0c0j..rec63a7",
"auth_time": 1565137817,
"cognito:username": "r..",
"email": "r..#gmail.com",
"email_verified": true,
"event_id": "c3ae..200f95f9921c",
"exp": ..2,
"iat": ..2,
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-..",
"phone_number": "+5...3",
"phone_number_verified": false,
"sub": "246e937..f4d8e6f4725b",
"token_use": "id",
},
},
"refreshToken": CognitoRefreshToken {
"token": "eyJjd...",
},
},
"storage": [Function MemoryStorage],
"userDataKey": "CognitoIdentityServiceProvider.12ddetjn0....userData",
"username": "r...ff",
}
To get all user attributes, you may be looking for the Auth.userAttributes() function. To use this you want something like this code:
const authUser = await Auth.currentAuthenticatedUser();
const attributes = await Auth.userAttributes(authUser);
// the next line is a convenience that moves the attributes into
// the authUser object
attributes.forEach((attr) => {
authUser.attributes[attr.Name] = attr.Value;
});
If you're still not getting the attributes you need, take a look here, and you can see that you can enable the reading of other attributes from the Amplify command line.
So, in the root of your project:
Type "amplify update auth" at the console.
Select "Walkthrough the auth configurations"
Step through making all the same selections as you've done before.
When it asks, "Do you want to specify the user attributes this app can read and write?" it's "Y", and then you select the attributes you want to be able to read.
When you finish the wizard, use "amplify push auth"
When that's completed, try re-running.
As an alternative to steps 1-4 above, you can also edit cli-inputs.json in the amplify\backend\auth<your auth config name> directory. It's in "userpoolClientReadAttributes". Simply add the attributes you would like to this array (e.g. "name").
This answer was verified with amplify CLI version 8.1.0.

AWS API Gateway: How to combine multiple Method Request params into a single Integration Request param

I'd like to use API Gateway as a proxy to S3. The bucket is keyed by a composite key made up of two parts like this: [userId]-[documentId].
UserId comes to API Gateway as a path parameter, documentId comes in as a request parameter, for example: [gateway-url]/user1?documentId=doc1
How can I combine the two so that the s3 lookup URL has the following format: https://[bucket-url]/user1-doc1?
Thank you.
Setup your Method Request to accept the path param {userid} and query param {docid}.
Setup your Integration Request to accept both method.request.querystring.docid and method.request.path.userid as URL path params.
Finally, setup your integration endpoint URL as https://your-url/{userid}-{docid}.
A swagger snippet for this is as follows-
"paths": {
"/concat-params/{userid}": {
"get": {
"parameters": [
{
"name": "userid",
"in": "path",
"required": true,
"type": "string"
},
{
"name": "docid",
"in": "query",
"required": false,
"type": "string"
}
],
"responses": {...},
"x-amazon-apigateway-integration": {
"responses": {...},
"requestParameters": {
"integration.request.path.userid":"method.request.path.userid",
"integration.request.path.docid":"method.request.querystring.docid"
},
"uri": "https:.../{userid}-{docid}",
...
}
}
}
Hope this helps,
Ritisha.