Preparing complex username, tenant and password for Passportjs authenticate - multi-tenant

Using Passportjs with Sails for user authentication on a JSON body (no query/form parameters).
The userObject is a bit more complex than just { "username": username, "password": password }. It is a tree and, among other data, contains a tenancy key, to be used on a multitenancy system.
{
"userObj" : {
"tenant" : id,
"email" : email,
"password" : password
}
Since Passport only accepts two parameters, username and password, I tried "cheating" on Passport configuration on the custom middleware:
passport.use(new LocalStrategy(
{
usernameField: 'customUserData',
passwordField: 'password'
},
function(usernameObject, password, done) {
var actualUser = usernameObject.email;
var tenant = usernameObject.tenant;
....
}
)
);
And during the actual authentication call, I rearranged:
req.body is JSON
req.body.customUserData = {};
req.body.customUserData.email= req.body.userObj.email;
req.body.customUserData.tenant = req.body.userObj.tenant;
req.body.password = req.body.userObj.password;
and then the authentication call:
passport.authenticate('local', function(err, user, info){ ... });
My expectation was that Passport would:
parse the JSON payload, grabbing customUserData
pass customerData as first parameter in the function (usernameObject)
allow me to get tenant and email for further processing
But this doesn't seem to be happening. Before I try debuggin other parts of the code, I need to know if this approach would theoretically work.
Or else, how would you configure and pass username + tenancy to Passport?

Related

Supabase realtime connection failing when custom JWT is used

I'm using supabase self-hosted and auth0 for my authentication instead of the default authentication provided by supabase. So I'm signing my auth0's payload with supabase secret and sending it in headers.
const payload = {
userId: user.email,
exp: Math.floor(Date.now() / 1000) + 60 * 60,
}
const accessToken = jwt.sign(payload, SUPABASE_SECRET_KEY)
const options = {}
if (accessToken) {
options.global = {
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
}
const supabase = createClient(supabaseUrl, supabaseAnonKey, options)
supabase.channel('custom-update-channel')
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'user_notifications' },
(payload) => {
console.log('Change received!', payload)
}
)
.subscribe()
I also enabled the RLS policy on my table. Using the above headers I'm able to query my database. Now I wanted to enable real-time on my table. But when I try to create a subscription with my custom headers, the Realtime web socket connection throws an Authentication error. When I don't send the custom JWT in the header, it works fine but I need my custom JWT's payload to be stored in the subscription table. realtime so that I can use it on my RLS Policy. What should I do to fix this?
Row Level Security (RLS) will fail if you are missing certain properties from your JWT. In your case you are probably using the auth.uid() function in your RLS which relies on a sub property for the user id. You need to add that to your JWT along with your existing userId property.
const payload = {
userId: user.email,
sub: user.id, // this must correspond with a user id in the table you are testing your RLS policy against
exp: Math.floor(Date.now() / 1000) + 60 * 60,
}
const accessToken = jwt.sign(payload, SUPABASE_SECRET_KEY)
When you use self-hosted supabase, kong will allow only an anon key or service key to be passed as an API key. To set our custom token we can do something like this:
const supabase = createClient(supabaseUrl, supabaseAnonKey, options)
supabase.realtime.accessToken = 'custom jwt'
Directly setting access token in realtime client.

How can we reset a Cognito User's password without using Cognito's forgot password flow?

I am working on a serverless project using node.js and AWS Lambda.
For auth, I am using AWS Cognito. (Frontend is a web-app in Vue.js on AWS Amplify).
I would like to write my own implementation of resetting a user's password who has forgotten their password.
Basically, the end-user fills up a form with their email. If email is in the system, I send them a reset link (which has a unique code I set in the DB).
I am aware of Cognito's Forgot Password flow and also a solution in which I can capture Cognito's "email sending" code and over-ride the email with my own template passing the code in the URL mentioned here.
I stumbled upon the adminSetUserPassword API which I was sure would work -- but no matter what I do, my lambda function does not get permissions to execute this operation.
This is my nodejs code:
import AWS from 'aws-sdk';
const COGNITO_POOL_ID = process.env.COGNITO_USERPOOL_ID;
const csp = new AWS.CognitoIdentityServiceProvider();
export async function resetUserPassword(username, newPassword) {
// Constructing request to send to Cognito
const params = {
Password: newPassword,
UserPoolId: COGNITO_POOL_ID,
Username: username,
Permanent: true,
};
await csp.adminSetUserPassword(params).promise();
return true;
}
This is my IAM permission for the lambda function (it is in serverless yml format):
CognitoResetPasswordIAM:
Effect: Allow
Action:
- cognito-idp:*
Resource:
- arn:aws:cognito-idp:us-east-1::*
(I will fine-tune the permissions once this works)
The following is the error message I am getting.
I am starting to feel that my approach to doing this is not the recommended way of doing things.
User: arn:aws:sts::[XXXXXXX]:assumed-role/[YYYYYYYYY]-us-east-1-lambdaRole/web-app-service-dev-resetPassword is not authorized to perform: cognito-idp:AdminSetUserPassword on resource: arn:aws:cognito-idp:us-east-1:[[XXXXXXX]]:userpool/us-east-1_ZZZZZZZZ
(Serverless has access to my AWS Access key with * permissions on * resources -- so I don't think I am missing any permissions there).
My questions:
Is this the recommended way of doing this?
Is it possible for me to configure permissions in a way that my lambda functions have the required permissions to perform this operation?
It turns out, you need to use the Amplify API and not the Cognito API.
This involves a couple of steps:
1. Configure your Cognito Amplify Service for Auth.
import Amplify, { Auth } from 'aws-amplify';
export function configureCognitoAuth() {
Amplify.configure({
Auth: {
region: process.env.COGNITO_REGION,
userPoolId: process.env.COGNITO_USERPOOL_ID,
mandatorySignIn: false,
userPoolWebClientId: process.env.COGNITO_CLIENT_ID,
authenticationFlowType: 'USER_PASSWORD_AUTH',
oauth: {
domain: process.env.COGNITO_APP_DOMAIN,
scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
responseType: 'code', // or 'token', note that REFRESH token will only be generated when the responseType is code
},
},
});
// You can get the current config object
Auth.configure();
}
2. Call the Auth.forgotPassword service to send the actual password here
import { Auth } from 'aws-amplify';
async function sendUserPasswordResetEmail(event) {
// Any validation checks, rate limits you want to check here, etc.
try {
configureCognitoAuth();
await Auth.forgotPassword(userId);
} catch (error) {
// An error occurred while sending the password reset email
}
}
3. Write a forgotPasswordEmailTrigger Cognito Hook
This replaces the default Cognito Reset password email with your own custom email.
This is also a lamdba method which you need to attach to the Cognito Custom Message trigger (from Cognito > General Settings > Triggers)
My code for this looks like so:
async function forgotPasswordEmailTrigger(event, context, callback) {
// Confirm it is a PreSignupTrigger
if (event.triggerSource === 'CustomMessage_ForgotPassword') {
const { userName } = event;
const passwordCode = event.request.codeParameter;
const resetUrl = `${BASE_URL}/password_reset/${userName}/${passwordCode}`;
let message = 'Your HTML email template goes here';
message = message
.replace(/{{passwordResetLink}}/g, resetUrl);
event.response.emailSubject = 'Email Subject here';
event.response.emailMessage = message;
}
// Return to Amazon Cognito
callback(null, event);
}
The event.request.codeParameter is where the code is returned from Cognito. I think there is a way to change this, but I didn't bother. I use the same code to verify in the next step.
4. Call the forgotPasswordSubmit method from the Amplify Auth service when a password reset request is sent to your backend
When the user clicks the URL, they come to the website and I pick up the code and the userID from the URL (from Step 3) and then verify the code + reset the password like so:
async function resetPassword(event) {
const { token, password, user_id } = event.body;
// Do your validations & checks
// Getting to here means everything is in order. Reset the password
try {
configureCognitoAuth(); // See step 1
await Auth.forgotPasswordSubmit(user_id, token, password);
} catch (error) {
// Error occurred while resetting the password
}
const result = {
result: true,
};
return {
statusCode: 200,
body: JSON.stringify(result),
};
}

Keystone.JS API User Authentication (not Admin-UI)

I would like to restrict my GraphQL API with User Authentication and Authorization.
All Keystone.JS documentation is talking about AdminUI authentication, which I'm not interested in at the moment.
Facts:
I want to have some social logins (no basic email/password)
I want to use JWT Bearer Tokens
Other than that you can suggest any possible way to achieve this.
My thoughts were:
I could have Firebase Authentication (which can use Google Sign-in, Apple Sign-in etc.) be done on the client-side (frontend) which would then upon successful authentication somehow connect this to my API and register user (?).
Firebase client SDK would also fetch tokens which I could validate on the server-side (?)
What is troubling is that I can't figure out how to do this in a GraphQL environment, and much less in a Keystone-wrapped GraphQL environment.
How does anyone do basic social authentication for their API made in Keystone?
Keystone authentication is independent of the Admin-UI. If you are not restricting your list with proper access control the authentication is useless. Default access is that it is open to all.
you can set default authentication at keystone level which is merged with the access control at list level.
Admin Ui Authentication
Admin UI only supports password authentication, meaning you can not go to /admin/signin page and authenticate there using other authentication mechanism. The Admin Ui is using cookie authentication. cookies are also set when you login using any other login method outside of admin-ui. This means that you can use any means of authentication outside of admin-ui and come back to admin ui and you will find yourself signed in.
Social Authentication:
Social authentication is done using passportjs and auth-passport package. there is documentation to make this work. Single Step Account Creation example is when you create user from social auth automatically without needing extra information (default is name and email). Multi Step Account Creation is when you want to capture more information like preferred username, have them accept the EULA or prompt for birthdate or gender etc.
JWT
I dont believe Keystone does pure JWT, all they do is set keystone object id in the cookie or the token is a signed version of item id (user item id) which can be decrypted only by the internal session manager using cookie secret.
Using Firebase to authenticate user
this is the flow of authentication after you create a custom mutation in keystone graphql.
client -> authenticate with Firebase -> get token -> send token to server -> server verifies the token with firebase using admin sdk -> authenticate existing user by finding the firebase id -> or create (single step) a user or reject auth call (multi step) and let client send more data like age, gender etc. and then create the user -> send token
here is the example of phone auth I did, you can also use passport based firebase package and implement your own solution.
keystone.extendGraphQLSchema({
mutations: [
{
schema: 'authenticateWithFirebase(token: String!): authenticateUserOutput',
resolver: async (obj, { token: fireToken }, context) => {
const now = Date.now();
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors || !data.firebaseUser || !data.firebaseUser.length) {
console.error(errors, `Unable to find user-authenticate`);
throw errors || new Error('unknown_user');
}
const item = data.firebaseUser[0];
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
{
schema: 'signupWithFirebase(token: String!, name: String!, email: String): authenticateUserOutput',
resolver: async (obj, { token: fireToken, name, email }, context) => {
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors) {
throw errors;
}
if (data.firebaseUser && data.firebaseUser.length) {
throw new Error('User already exist');
}
const { errors: signupErrors, data: signupData } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
mutation createUser($data: UserCreateInput){
user: createUser(data: $data) {
id
name
firebaseId
email
phone
}
}`,
variables: { data: { name, phone: phone, firebaseId: uid, email, wallet: { create: { walletId: generateWalletId() } }, cart: { create: { lineItems: { disconnectAll: true } } } } },
});
if (signupErrors || !signupData.user) {
throw signupErrors ? signupErrors.message : 'error creating user';
}
const item = signupData.user;
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
],
})

loopback modify access token

I want to add more details to the access token which is generated on calling the user login method. I have extended the builtin user model with my own Customer model. My login method looks like below:
getToken(userData, data, callback) {
const Customer = app.models.Customer;
Customer.login(
{ email: userData.email, password: userData.password },
function (error, accessToken) {
if (error) {
callback(error);
} else {
callback(null, accessToken);
}
}
)
}
I want to add data to the accessToken on login. How do I achieve that ? Basically I want to add more information to accessToken other than userId. I also want to retain userId in the token.
Thanks

How to add claims to access token get from IdentityServer3 using resource owner flow with javascript client

I use the resource owner flow with IdentityServer3 and send get token request to identity server token endpoint with username and password in javascript as below:
function getToken() {
var uid = document.getElementById("username").value;
var pwd = document.getElementById("password").value;
var xhr = new XMLHttpRequest();
xhr.onload = function (e) {
console.log(xhr.status);
console.log(xhr.response);
var response_data = JSON.parse(xhr.response);
if (xhr.status === 200 && response_data.access_token) {
getUserInfo(response_data.access_token);
getValue(response_data.access_token);
}
}
xhr.open("POST", tokenUrl);
var data = {
username: uid,
password: pwd,
grant_type: "password",
scope: "openid profile roles",
client_id: 'client_id'
};
var body = "";
for (var key in data) {
if (body.length) {
body += "&";
}
body += key + "=";
body += encodeURIComponent(data[key]);
}
xhr.setRequestHeader("Authorization", "Basic " + btoa(client_id + ":" + client_secret));
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send(body);
}
The access token is returned from identity server and user is authenticated. Then I use this token to send request to my Web Api.
The problem is that when I check if the user is assigned a role, I find the claim doesn't exist.
[Authorize]
// GET api/values
public IEnumerable<string> Get()
{
var id = RequestContext.Principal as ClaimsPrincipal;
bool geek = id.HasClaim("role", "Geek"); // false here
bool asset_mgr = id.HasClaim("role", "asset_manager"); // false here
return new string[] { "value1", "value2" };
}
Here is how the client is defined in identity server.
new Client
{
ClientName = "Client",
ClientId = "client_id",
Flow = Flows.ResourceOwner,
RequireConsent = false,
AllowRememberConsent = false,
AllowedScopes = new List<string>
{
"openid",
"profile",
"roles",
"sampleApi"
},
AbsoluteRefreshTokenLifetime = 86400,
SlidingRefreshTokenLifetime = 43200,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
RefreshTokenExpiration = TokenExpiration.Sliding,
ClientSecrets = new List<Secret>
{
new Secret("4C701024-0770-4794-B93D-52B5EB6487A0".Sha256())
},
},
and this is how the user is defined:
new InMemoryUser
{
Username = "bob",
Password = "secret",
Subject = "1",
Claims = new[]
{
new Claim(Constants.ClaimTypes.GivenName, "Bob"),
new Claim(Constants.ClaimTypes.FamilyName, "Smith"),
new Claim(Constants.ClaimTypes.Role, "Geek"),
new Claim(Constants.ClaimTypes.Role, "Foo")
}
}
How can I add claims to the access_token in this case? Thanks a lot!
I have just spent a while figuring this out myself. #leastprivilege's comment on Yang's answer had the clue, this answer is just expanding on it.
It's all down to how the oAuth and OIDC specs evolved, it's not an artefact of IdentityServer (which is awesome).
Firstly, here is a fairly decent discussion of the differences between identity tokens and access tokens: https://github.com/IdentityServer/IdentityServer3/issues/2015 which is worth a read.
With Resource Owner flow, like you are doing, you will always get an Access Token. By default and per the spec, you shouldn't include claims in that token (see the above link for why). But, in practice, it is very nice when you can; it saves you extra effort on both client and server.
What Leastprivilege is referring to is that you need to create a scope, something like this:
new Scope
{
Name = "member",
DisplayName = "member",
Type = ScopeType.Resource,
Claims = new List<ScopeClaim>
{
new ScopeClaim("role"),
new ScopeClaim(Constants.ClaimTypes.Name),
new ScopeClaim(Constants.ClaimTypes.Email)
},
IncludeAllClaimsForUser = true
}
And then you need to request that scope when you ask for the token. I.e. your line
scope: "openid profile roles", should change to scope: "member", (well, I say that - scopes play a dual role here, as far as I can see - they are also a form of control, i.e. the client is asking for certain scopes and can be rejected if it is not allowed those but that is another topic).
Note the important line that eluded me for a while, which is Type = ScopeType.Resource (because Access Tokens are about controlling access to resources). This means it will apply to Access Tokens and the specified claims will be included in the token (I think, possibly, against spec but wonderfully).
Finally, in my example I have included both some specific claims as well as IncludeAllClaimsForUser which is obviously silly, but just wanted to show you some options.
I find I can achieve this by replacing the default IClaimsProvider of IdentityServerServiceFactory.
The cusomized IClaimsProvider is as below:
public class MyClaimsProvider : DefaultClaimsProvider
{
public MaccapClaimsProvider(IUserService users) : base(users)
{
}
public override Task<IEnumerable<Claim>> GetAccessTokenClaimsAsync(ClaimsPrincipal subject, Client client, IEnumerable<Scope> scopes, ValidatedRequest request)
{
var baseclaims = base.GetAccessTokenClaimsAsync(subject, client, scopes, request);
var claims = new List<Claim>();
if (subject.Identity.Name == "bob")
{
claims.Add(new Claim("role", "super_user"));
claims.Add(new Claim("role", "asset_manager"));
}
claims.AddRange(baseclaims.Result);
return Task.FromResult(claims.AsEnumerable());
}
public override Task<IEnumerable<Claim>> GetIdentityTokenClaimsAsync(ClaimsPrincipal subject, Client client, IEnumerable<Scope> scopes, bool includeAllIdentityClaims, ValidatedRequest request)
{
var rst = base.GetIdentityTokenClaimsAsync(subject, client, scopes, includeAllIdentityClaims, request);
return rst;
}
}
Then, replace the IClaimsProvider like this:
// custom claims provider
factory.ClaimsProvider = new Registration<IClaimsProvider>(typeof(MyClaimsProvider));
The result is that, when the request for access token is sent to token endpoint the claims are added to the access_token.
Not only that I tried other methods, I tried all possible combinations of scopes etc. All I could read in the access token was "scope", "scope name", for Resource Flow there were no claims I have added period.
I had to do all this
Add custom UserServiceBase and override AuthenticateLocalAsync since I have username/password there and I need both to fetch things from the database
Add claims that I need in the same function (this on itself will not add claim to Access Token, however you will able to read them in various ClaimsPrincipal parameters around)
Add custom DefaultClaimsProvider and override GetAccessTokenClaimsAsync where ClaimsPrincipal subject contains the claims I previously set, I just take them out and put again into ølist of claims for the result.
I guess this last step might be done overriding GetProfileDataAsync in the custom UserServiceBase, but the above just worked so I did not want to bother.
The general problem is not how to set claims, it is where you populate them. You have to override something somewhere.
This here worked for me since I needed data from a database, someone else should populate claims elsewhere. But they are not going to magically appear just because you nicely set Scopes and Claims Identity Server configurations.
Most of the answers say not a word about where to set the claim values properly. In each particular override you have done, the passed parameters, when they have claims, in the function are attached to identity or access token.
Just take care of that and all will be fine.

Resources