Servicestack GlobalRequestFilters populating additional user auth data into Jwt tokens - asp.net-core-mvc

I want to add additional properties to the response when a user logs in.
When calling https://Servicestackservice/auth/credentials?userName=****&password=**** I get the below response. I want to add 2 additional values. DateFormat & TimeZone
{
"userId": "21",
"sessionId": "****",
"userName": "SystemAdmin",
"displayName": "System Admin",
"referrerUrl": null,
"bearerToken": "****",
"refreshToken": *",
"profileUrl": *",
"roles": [ View
],
"permissions": [ View
],
"responseStatus": {
"errorCode": null,
"message": null,
"stackTrace": null,
"errors": null,
"meta": null
},
"meta": null
}
I found an example from the SS forums. I had to modify it some to make it run.
From the SS docs
Modifying the Payload
Whilst only limited info is embedded in the payload by default, all matching AuthUserSession properties embedded in the token will also be populated on the Session, which you can add to the payload using the CreatePayloadFilter delegate. So if you also want to have access to when the user was registered you can add it to the payload with:
I am hoping this is how i get them into the "matching AuthUserSession"
this.GlobalRequestFilters.Add(async (req, res, requestDto) =>
{
AuthFilter.AuthResponse(req, res, requestDto);
});
public static void AuthResponse(IRequest req, IResponse res, object response)
{
var authRes = response as Authenticate;
if (authRes == null || authRes.UserName == null)
{
return;
}
var session = (CustomUserSession)req.GetSession();
if (session != null && session.UserAuthId != null)
{
//General Format for US
string dformat = "g";
using (var db = HostContext.TryResolve<IDbConnectionFactory>().Open())
{
var userAuthExt = db.Single<UserAuthExtension>(ext => ext.UserAuthId == int.Parse(session.UserAuthId));
if (userAuthExt != null)
{
dformat = userAuthExt.DateTimeFormat;
}
}
authRes.Meta = new Dictionary<string, string> {{"TimeZone", session.TimeZone}, {"DateFormat", dformat}};
}
}
Adding this to try to get the JWT tokens to hold the new data. Examining the payload i can see the 2 new values are added to the list.
new JwtAuthProvider(AppSettings)
{
CreatePayloadFilter = (payload, session) =>
{
if (session != null && session.UserAuthId != null)
{
//General Format for US
string dformat = "g";
using (var db = HostContext.TryResolve<IDbConnectionFactory>().Open())
{
var userAuthExt = db.Single<UserAuthExtension>(ext => ext.UserAuthId == int.Parse(session.UserAuthId));
if (userAuthExt != null)
{
dformat = userAuthExt.DateTimeFormat;
}
}
payload["TimeZone"] = ((AuthUserSession) session).TimeZone;
payload["DateFormat"] = dformat;
}
},

You should link to the docs you're referring to, which I believe is ServiceStack's JWT Modifying the Payload docs. Although it's not clear which example in the Customer Forums you're referring to.
It's also not clear what the question is, I'm assuming it's this statement:
When calling /auth/credentials?userName=****&password=**** I do not see the new values.
Where exactly are you expecting these values? If you're authenticating by credentials you're not Authenticating by JWT so you will not have these additional properties populated on your User Session. If they're embedded in your JWT's body payload then as TimeZone is a AuthUserSession property, it should be populated if it was contained within the JWT payload:
case "TimeZone":
authSession.TimeZone = entry.Value;
break;
But DateFormat is not an AuthUserSession property so you will need to populate it manually by providing an implementation for PopulateSessionFilter, e.g:
new JwtAuthProvider(AppSettings)
{
PopulateSessionFilter = (session,payload,req) =>
session.Meta["DateFormat"] = payload["DateFormat"];
}
But these properties are only going populated in the Users Session when authenticating via JWT.
To help diagnose any issues you should but a breakpoint in your CreatePayloadFilter to see what you've populated the JWT payload with and conversely put a breakpoint in your PopulateSessionFilter to inspect what's contained in the payload and resulting populated session.

Related

How to access identity provider (idp) claim from .net Core web API in IdentityServer4?

In my .Net Core web API protected by IdentityServer4, I need to decide what identity provider (Google, Windows, or local, for instance) authenticated the user. So far, I am not sure how to do that.
If I search for idp claim from access_token in a controller, as shown below, I can see the claim value correctly
var accessToken = await HttpContext.GetTokenAsync("access_token");
var token = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
var claim = token.Claims.First(c => c.Type == "idp").Value;
But if I try to find it using AuthorizationHandlerContext in a non-controller class in API as following, as shown in code below, it is not there
var identity = context.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
// var v = identity.FindFirst("idp").Value;
}
So looks like that idp is indeed in the token, it just not accessible from the non-controller class where it is needed. How do I get idp from non-controller class in API?
UPDATE - 1
Here is my ConfigureService in my API
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true; // test only
services.AddControllers();
services.AddControllers()
.AddNewtonsoftJson(
options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
});
services.Configure<QLHostOptions>(Configuration.GetSection(QLHostOptions.Host));
services.AddAuthentication("Bearer").AddJwtBearer("Bearer", options =>
{
options.Authority = Configuration.GetSection(QLHostOptions.Host).Get<QLHostOptions>().IdentityGateway;
options.SaveToken = true;
// test only
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
}).AddOpenIdConnect(options =>
{
options.ClaimActions.Remove("aud");
});
services.AddTransient<IAuthorizationPolicyProvider, QLPolicyProvider>();
services.AddTransient<IAuthorizationHandler, QLPermissionHandler>();
services.AddTransient<gRPCServiceHelper>();
}
UPDATE-2
Changed ...Remove("idp") to inside AddJwtBearer, as Tory suggested, but it doesn't take it (see screenshot below):
and here is the access token from API
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjBFM0Y2MkRGMTdFQUExQURFRTc1NDQzQzQ0M0YxRkU2IiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2NDAwMzExMDcsImV4cCI6MTY0MDAzNDcwNywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NjAwNSIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0OjYwMDUvcmVzb3VyY2VzIiwiY2xpZW50X2lkIjoibXZjIiwic3ViIjoiZGM0YWI1OGMtNGVjMC00ZTAyLWIxM2YtYzEyYzk1MzJlNzcyIiwiYXV0aF90aW1lIjoxNjQwMDMxMTA2LCJpZHAiOiJHb29nbGUiLCJBc3BOZXQuSWRlbnRpdHkuU2VjdXJpdHlTdGFtcCI6IjJEQ0hXNVRER1E3NDNSUEpOWE43SVJIWlRIVllIUTRJIiwibmFtZSI6IkxpZmVuZyBYdSIsImVtYWlsIjoibGlmZW5neHUyNkBnbWFpbC5jb20iLCJyb2xlIjoiUUxBZG1pbiIsInByZWZlcnJlZF91c2VybmFtZSI6IjM1ZGJkMmY2LTlmNDUtNDJhYy04M2EzLTgzZmUyMTFjNTNiNSIsIklzRW5hYmxlZCI6IlRydWUiLCJRaWNMaW5rVUlEIjoiIiwianRpIjoiQzE3Qjc2QzQ0NjA4MzkxMDBENEExMEM4Q0YwQzA1NDEiLCJzaWQiOiIzMEY5NTA5NzQ3OUUxMzAyMUVBQTdDOTAzNzg4MDcxNiIsImlhdCI6MTY0MDAzMTEwNywic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIiwiUWljTGlua0NJRCIsIlFpY0xpbmtVSUQiLCJyb2xlcyIsIklzRW5hYmxlZCIsIkxpZmVuZ0FQSSIsIlFpY0xpbmtBUEkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiZXh0ZXJuYWwiXX0.boZCqYImWfkE48X5UgFOAAz9bR6CH2cwAYHGd4Ykg0vDH9qnYdje5Zmqov4HpINsu_rt16zxAX_JCEn0hvdznXK2NQyZSBGsjF0tcMgtOY0__kAfhpOT-fORakiIjeMWIKG7tPEHCxSib0wNuMNw6i3o1giAnPt0ch2DH0fBtaEYkq4MRKMCteFuqbX0cogXIuMewNywMvrHv4_MixhMy3L8_xIwFvTZ67jhUn4Fd5X58-jc-RPNudcP95XIjzHm9OzWfgegV1IAKjsv98XEYX1pUxm-nrOMgYWxEJSyxEpp0L_9RzKTr_LZ-ep-x5QRvVewgiozJV3mse0pHgTjbw"
By default many of the more internal claims in a token are removed from the User ClaimsPrinicpal claims.
If you want to get a specific claim into your user, you can use in the client:
}).AddOpenIDConnect(options =>
{
//Will result in that the aud claim is not removed.
options.ClaimActions.Remove("idp");
...
secondly, some of the claims are renamed and if you want to disable that renaming, you can add:
// By default, Microsoft has some legacy claim mapping that converts
// standard JWT claims into proprietary ones. This removes those mappings.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
For the API you should not need to do anything special to get the idp claim. I just ran a test with this setup in .NET 5:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMyJwtBearer(opt =>
{
opt.IncludeErrorDetails = true;
opt.MapInboundClaims = false;
opt.TokenValidationParameters.RoleClaimType = "role";
opt.TokenValidationParameters.NameClaimType = "name";
opt.Audience = "paymentapi";
opt.Authority = "https://localhost:6001";
});
services.AddControllers();
}
I did give it a test on .NET 5 and if I have this access token:
{
"nbf": 1640033816,
"exp": 1640037416,
"iss": "https://localhost:6001",
"aud": "paymentapi",
"client_id": "clientcredentialclient",
"managment": "yes",
"email": "tore#tn-data.se",
"name": "tore nestenius",
"idp": "Google",
"role": [
"admin",
"developer",
"support"
],
"website": "https://www.tn-data.se",
"jti": "5DC46A29372031F0AA6F7B62B5FDCCD6",
"iat": 1640033816,
"scope": [
"payment"
]
}
Then my user in my API controller contains the idp claim:

How to remove unwanted keys from rest-assured response object and assert remaining object data with constant variable having json string using java

In rest-assured test cases I am getting response as mentioned, where I want to remove keys such as "updated_at", "deleted_at", "created_at" and "notice" and then assert this response object with expected json string constant which contains 'settings'
{
"notice": "The Settings are updated successfully.",
"settings": {
"push_notification": {
"enabled": true,
"credentials": [{
"key": "value"
}],
"service_name": "API Testing"
},
"created_at": "2019-05-04T14:52:32.773Z",
"deleted_at": "false",
"updated_at": "2019-05-07T11:23:22.781Z"
}
}
For given response the expected json string is...
public static String SETTING_EXPECTED = "{\"push_notification\": {\"enabled\": true, \"credentials\": [{\"key\": \"value\"}], \"service_name\": \"API Testing\"}}"
Please help me with creating a common method using java which can be reuse for response assertions in all the test cases.
To delete keys from response you can use below code I am using jayway jsonpath library, you need to pass Json Response and field name jsonPath, in case your it will be "$.settings.created_at" :
public String deleteFieldNameFromResponse(String jsonResponse, String fieldToDelete)
throws ParseException, FileNotFoundException, IOException {
Object obj = null;
JSONParser parser = new JSONParser();
JsonPath jsonPath = null;
DocumentContext docCtx = null;
obj = parser.parse(jsonResponse);
docCtx = JsonPath.parse(obj);
docCtx.delete(fieldToDelete);
jsonPath = JsonPath.compile("$");
return docCtx.read(jsonPath).toString();
}

Getting Daily Limit for unauthenticated use Exceeded

Getting Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup
when executing the following code given in the Xamarin demo
async void OnAuthenticationCompleted (object sender, AuthenticatorCompletedEventArgs e)
{
if (e.IsAuthenticated) {
// If the user is authenticated, request their basic user data from Google
// UserInfoUrl = https://www.googleapis.com/oauth2/v2/userinfo
var request = new OAuth2Request ("GET", new Uri (Constants.UserInfoUrl), null, e.Account);
var response = await request.GetResponseAsync ();
if (response != null) {
// Deserialize the data and store it in the account store
// The users email address will be used to identify data in SimpleDB
string userJson = response.GetResponseText ();
App.User = JsonConvert.DeserializeObject<User> (userJson);
e.Account.Username = App.User.Email;
AccountStore.Create ().Save (e.Account, App.AppName);
}
}
// If the user is logged in navigate to the TodoList page.
// Otherwise allow another login attempt.
App.SuccessfulLoginAction.Invoke ();
}
This is the error I am getting
"error":{
"errors": [
{
"domain": "usageLimits",
"reason": "dailyLimitExceededUnreg",
"message" :"Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup."
"extendedHelp" : "https://code.google/apis/console"
}
],
"code":403,
"message": "Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup."
}
I corrected the redireturl to "http://example.com/oauth2callback" and in the ios project modified the ATS setting by adding the following key in info.plist
NSAppTransportSecurity -- Dictionary
NSAllowsArbitraryLoadsInWebContent -- Boolean -- Yes

Loopback custom password validation

very simple question: if I try to validate a password in a User model it seems I can only validate the already encrypted password?
So for example if I use
Customer.validatesLengthOf('password', { min: 8, message: 'Too short' })
Then the encrypted password is checked (which is always longer than 8 characters), so no good... If I try to use a custom validation, how can I get access to the original password (the original req.body.password basically)?
EDIT (August 20, 2019): I am unsure if this is still an issue in the latest loopback releases.
In fact, this is a known problem in loopback. The tacitly approved solution is to override the <UserModel>.validatePassword() method with your own. YMMV.
akapaul commented on Jan 10, 2017 •
I've found another way to do this. In common model User there is a
method called validatePassword. If we extend our UserModel from User,
we can redefine this method in JS, like following:
var g = require('loopback/lib/globalize');
module.exports = function(UserModel) {
UserModel.validatePassword = function(plain) {
var err,
passwordProperties = UserModel.definition.properties.password;
if (plain.length > passwordProperties.max) {
err = new Error (g.f('Password too long: %s (maximum %d symbols)', plain, passwordProperties.max));
err.code = 'PASSWORD_TOO_LONG';
} else if (plain.length < passwordProperties.min) {
err = new Error(g.f('Password too short: %s (minimum %d symbols)', plain, passwordProperties.min));
err.code = 'PASSWORD_TOO_SHORT';
} else if(!(new RegExp(passwordProperties.pattern, 'g').test(plain))) {
err = new Error(g.f('Invalid password: %s (symbols and numbers are allowed)', plain));
err.code = 'INVALID_PASSWORD';
} else {
return true;
}
err.statusCode = 422;
throw err;
};
};
This works for me. I don't think that g (globalize) object is required
here, but I added this, just in case. Also, I've added my validator
options in JSON definition of UserModel, because of Loopback docs
For using the above code, one would put their validation rules in the model's .json definition like so (see max, min, and pattern under properties.password):
{
"name": "UserModel",
"base": "User",
...
"properties": {
...
"password": {
"type": "string",
"required": true,
...
"max": 50,
"min": 8,
"pattern": "(?=.*[A-Z])(?=.*[!##$&*])(?=.*[0-9])(?=.*[a-z])^.*$"
},
...
},
...
}
ok, no answer so what I'm doing is using a remote hook to get access to the original plain password and that'll do for now.
var plainPwd
Customer.beforeRemote( 'create', function (ctx, inst, next) {
plainPwd = ctx.req.body.password
next()
})
Then I can use it in a custom validation:
Customer.validate( 'password', function (err, res) {
const pattern = new RegExp(/some-regex/)
if (plainPwd && ! pattern.test( plainPwd )) err()
}, { message: 'Invalid format' })
Ok I guess the above answer is quite novel and obviously is accepted, but If you want a real easy solution with just some basic validations done and not much code then loopback-mixin-complexity is the solution for you.
If you don't want to create another dependency then you can go ahead with a custom mixin, that you can add into your user model or any other model where you need some kind of validation and it would do the validation for you.
Here's a sample code for how to create such mixin
module.exports = function(Model, options) {
'use strict';
Model.observe('before save', function event(ctx, next) { //Observe any insert/update event on Model
if (ctx.instance) {
if(!yourValidatorFn(ctx.instance.password) )
next('password not valid');
else
next();
}
else {
if(!yourValidatorFn(ctx.data.password) )
next('password not valid');
else
next();
}
});
};

Parse.com manipulate Response Object

I am trying to work Ember with Parse.com using
ember-model-parse-adapter by samharnack.
I add added a function to make multiple work search(like search engine) for which I have defined a function on cloud using Parse.Cloud.define and run from client.
The problem is the Array that my cloud response returns is not compatible with Ember Model because of two attributes they are __type and className. how can I modify the response to get response similar to that i get when I run a find query from client. i.e without __type and className
Example responses
for App.List.find() = {
"results":[
{
"text":"zzz",
"words":[
"zzz"
],
"createdAt":"2013-06-25T16:19:04.120Z",
"updatedAt":"2013-06-25T16:19:04.120Z",
"objectId":"L1X55krC8x"
}
]
}
for App.List.cloudFunction("sliptSearch",{"text" : this.get("searchText")})
{
"results":[
{
"text":"zzz",
"words":[
"zzz"
],
"createdAt":"2013-06-25T16:19:04.120Z",
"updatedAt":"2013-06-25T16:19:04.120Z",
"objectId":"L1X55krC8x",
"__type" : Object, //undesired
"className" : "Lists" //undesired
}
]
}
Thanks Vlad something like this worked for me for array
resultobj = [];
searchListQuery.find({
success: function(results) {
for( var i=0, l=results.length; i<l; i++ ) {
temp = results.pop();
resultobj.push({
text: temp.get("text"),
createdAt: temp.createdAt,
updatedAt: temp.updatedAt,
objectId: temp.id,
words: "",
hashtags: ""
});
}
In your cloud code before you make any response, create and object and extract from it the attributes/members you need and then response it. like so:
//lets say result is some Parse.User or any other Parse.Object
function(result)
{
var responseObj = {};
responseObj.name = responseObj.get("name");
responseObj.age = responseObj.get("age");
responseObj.id = responseObj.id;
response.success(responseObj);
}
on the response side you will get {"result": {"name": "jhon", "age": "26", "id": "zxc123s21"}}
Hope this would help you

Resources