I'm using NextAuth with the Prisma adapter and AWS Cognito and it works perfectly, but my problem is that my User model doesn't get updated if I change the groups on Cognito. This is how I configured NextAuth:
// I copied the original and changed some of the fields
export type CognitoProfile = {
email: string;
sub: string;
preferred_username: string;
"cognito:groups": string[];
};
const CognitoProvider = (
options: OAuthUserConfig<CognitoProfile>
): OAuthConfig<CognitoProfile> => {
return {
id: "cognito",
name: "Cognito",
type: "oauth",
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
idToken: true,
profile: (profile) => {
return {
id: profile.sub,
name: profile.preferred_username,
email: profile.email,
image: "",
roles: profile["cognito:groups"],
};
},
options,
};
};
export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session: ({ session, user }) => {
console.log(`User: ${JSON.stringify(user)}`);
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
adapter: PrismaAdapter(prisma),
providers: [
CognitoProvider({
clientId: process.env.COGNITO_CLIENT_ID!,
clientSecret: process.env.COGNITO_CLIENT_SECRET!,
issuer: process.env.COGNITO_ISSUER,
}),
],
};
This works perfectly when a new user logs in (their groups are saved properly).
The problem is that the database is not updated when I log out and log back in after I add/remove group(s) to a Cognito user. This problem is not Cognito-specific it would be the same with things like Keycloak.
I checked the NextAuth docs, but I didn't find a solution for this. What's the recommended way of keeping the User model up to date? I don't want to reinvent the wheel 😅
Related
I am attempting to build a NextJS application that implements NextAuth. I am encountering the following error in my [...nextauth].ts when configuring my callbacks:
Type error: Property 'role' does not exist on type 'User | AdapterUser'.
Property 'role' does not exist on type 'User'.
56 | jwt: async ({ token, user }) => {
57 | // First time JWT callback is run, user object is available
> 58 | if (user && user.id && user.role) {
| ^
59 | token.id = user.id;
60 | token.role = user.role;
61 | }
The complete callback section of code looks like this:
callbacks: {
jwt: async ({ token, user }) => {
// First time JWT callback is run, user object is available
if (user && user.id && user.role) {
token.id = user.id;
token.role = user.role;
}
return token;
},
session: async ({ session, token }) => {
if (token && token.id && token.role) {
session.id = token.id;
session.role = token.role;
}
return session;
},
},
I am using the CredentialProvider with an email and a password. Here is authorize:
async authorize(credentials) {
if (!credentials || !credentials.email) return null;
const dbCredentials = await executeAccountQuery(
`SELECT password FROM auth WHERE email=?`,
[credentials.email]
);
if (Array.isArray(dbCredentials) && "password" in dbCredentials[0]) {
const isValid = await compare(
credentials.password,
dbCredentials[0].password
);
if (isValid) {
return {
id: "5",
role: 99,
name: "John Smith",
email: credentials.email,
};
}
return null;
}
return null; // login failed
},
Because of the way the authorize function is working, I know for a fact that the User object will have a role appended to it (because I have tested it), but I cannot figure out a way to handle this error and get rid it.
Similarily, I also get an error with the session callback where the session.id and session.role are also not present on Session.
After a lot of digging through the internet and some trial and error, I was able to figure out how to solve this problem.
You will need to manually extend the Session, User and JWT types by doing the following:
Create types/next-auth.d.ts in your root project directory.
Insert the following into the file:
import { Session } from "next-auth";
import { JWT } from "next-auth/jwt";
declare module "next-auth" {
interface Session {
id: string;
role: number;
}
interface User {
id: string;
role: number;
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string;
role: number;
}
}
Run npm run build and verify that it builds correctly.
Type Definition:
types/next-auth.d.ts:
import { DefaultUser } from 'next-auth';
declare module 'next-auth' {
interface Session {
user?: DefaultUser & { id: string; role: string };
}
interface User extends DefaultUser {
role: string;
}
}
Usage:
async session({ session, user }) {
if (session?.user) {
session.user.id = user.id;
session.user.role = user.role;
}
return session;
}
I'm trying to get some additional data from a SAML login provider. I can see this data in the client but I fail to see how to get this in the back end (firebase functions).
I'm using these in the FE
"firebase": "^9.8.2",
"firebase-functions": "^3.14.1",
And this in the BE
"firebase-admin": "^10.2.0",
"firebase-functions": "^3.21.2",
This is the data and how I get it in the client:
async myproviderSignIn() {
const provider = new SAMLAuthProvider('saml.myprovider');
const auth = getAuth();
const userCredential = await signInWithPopup(auth, provider);
const credential = SAMLAuthProvider.credentialFromResult(userCredential);
if (!environment.production) {
console.log('User:', userCredential, credential);
console.log(
'getAdditionalUserInfo:',
getAdditionalUserInfo(userCredential)
);
}
}
This is what I'm after and logged by getAdditionalUserInfo in the client:
{
"isNewUser": false,
"providerId": "saml.myprovider",
"profile": {
"urn:schac:attribute-def:schacPersonalUniqueCode": "urn:schac:personalUniqueCode:nl:local:diy.myproviderconext.nl:studentid:123456",
"urn:oid:1.3.6.1.4.1.25178.1.2.9": "diy.myproviderconext.nl",
"urn:oid:1.3.6.1.4.1.25178.1.2.14": "urn:schac:personalUniqueCode:nl:local:diy.myproviderconext.nl:studentid:123456",
"urn:oid:0.9.2342.19200300.100.1.3": "student1#diy.myproviderconext.nl",
"urn:oid:1.3.6.1.4.1.5923.1.1.1.1": [
"student",
"employee",
"staff",
"member"
],
"urn:mace:dir:attribute-def:eduPersonAffiliation": [
"student",
"employee",
"staff",
"member"
],
"urn:mace:dir:attribute-def:sn": "One",
"urn:mace:dir:attribute-def:givenName": "Student",
"urn:oid:2.5.4.42": "Student",
"urn:mace:dir:attribute-def:mail": "student1#diy.myproviderconext.nl",
"urn:oid:2.5.4.4": "One",
"urn:mace:terena.org:attribute-def:schacHomeOrganization": "diy.myproviderconext.nl"
}
}
Finally this is my BE on user create trigger. It creates a DB record of the user when a new user is created in Firebase auth. I'd wish to map some of the properties shown above here to the user record in the DB.
export const onCreate = functions.auth
.user()
.onCreate((user: UserRecord, context: EventContext) => {
const timestamp = serverTimestamp();
const dbUser: DbUser = {
uid: user.uid,
name: user.displayName || '',
firstName: user.displayName || '',
lastName: '',
email: user.email,
photoURL: user.photoURL,
emailVerified: user.emailVerified,
createdDate: timestamp,
lastSeen: timestamp,
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
};
// Get additional user data from the UserCredential
// const additionalUserInfo = getAdditionalUserInfo(user); ???
const result = getFirestore()
.collection(constants.dbCollections.users)
.doc(user.uid)
.set(dbUser);
return result;
});
How do I access these additional properties in my cloud function without relying on the client?
the problem you try to solve is something explained in here:
https://firebase.google.com/docs/auth/admin/custom-claims#node.js
some of the data you described below is stored within the auth.user
const dbUser: DbUser = {
uid: user.uid,
name: user.displayName || '',
firstName: user.displayName || '',
lastName: '',
email: user.email,
photoURL: user.photoURL,
emailVerified: user.emailVerified,
createdDate: timestamp,<-----------------------------------------------here
lastSeen: timestamp,<--------------------------------------------------and here
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
};
but the data i marked as "here" needs to be created via "custom user claims" but its under the FireBaseAnalytics tab neither auth. nor firestore if you insist to retrive this data from Firebase.
But for this data :
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
you should use FireStore or FirebaseRealTimeDatabase i repeat if you insist of getting those data from FireBase because of the 1000byte policy of Custom Claims.
i hope i do understand your question well. But im sure if you add those properties with this
// Set admin privilege on the user corresponding to uid.
getAuth()
.setCustomUserClaims(uid, { admin: true })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
you can retrive it with .auth token
I've been getting a lot of problems trying to add my Cognito user sub UUID into my Sequelize table. I have seen solutions for Sequelize to auto-generate their own UUID but not for custom ones? I am pushing the AWS sub UUID from my client-side as a payload in express.
Model file:
function User(sequelize) {
const User = sequelize.define(
"User",
{
id: {
type: DataTypes.UUID,
primaryKey: true,
allowNull: false,
},
Client Side
console.log('result: ', user);
console.log('result: ', user.userSub, typeof user.userSub);
console.log('uuid result: ', uuidParse(user.userSub), typeof uuidParse(user.userSub));
let payload = {
id: user.userSub,
username: username,
email: email,
birthday: birthdate
}
const request = {
url: `/api/user`,
method: 'POST',
payload,
};
query(request);
Error:
"name":"SequelizeValidationError","errors":[{"message":"User.id cannot be null","type":"notNull Violation","path":"id","value":null,"origin":"CORE"
Thank you
Just figured it out, it seems I forget to add id in my parameter in my createUser method in my model. Everything is working fine
User.createUser = async function ({ **id**, username, email, bio, birthday, gender }) {
return await this.create({
**id**,
username,
email,
bio,
birthday,
gender
});
};
I am working with xstate with Nextjs. Now I am stuck somewhere.
import { assign, createMachine, interpret } from "xstate";
export interface toggleAuth {
isAuthenticated: boolean;
user: {
name: string | undefined;
};
}
// console.log(getCachedData());
export const authMachine = createMachine<toggleAuth>({
id: "auth",
initial: "unauthenticated",
context: {
isAuthenticated: false,
user: {
name: undefined,
},
},
states: {
authenticated: {
on: {
toggle: {
target: "unauthenticated",
},
},
entry: assign({
user: (ctx) => (ctx.user = { name: "Pranta" }),
isAuthenticated: (ctx) => (ctx.isAuthenticated = true),
}),
},
unauthenticated: {
on: {
toggle: {
target: "authenticated",
},
},
entry: assign({
user: (ctx) => (ctx.user = { name: undefined }),
isAuthenticated: (ctx) => (ctx.isAuthenticated = false),
}),
},
},
});
const service = interpret(authMachine);
service.onTransition((state) => console.log(state));
So I was watching the docs. According to them, whenever I transition from unauthenticated to authenticated and authenticated to unauthenticated, it should console log it for me. But it doesn't. It does only one time. What's happening here. Also, is it okay to define my machine like this? Thanks in advance.
It's not logging because you're not changing state; no event is ever being sent.
Please re-read the documentation on assigning to context - you are mutating context instead of assigning new values; the assigners should always be pure.
If you want to see the state change, you need to send a toggle event in this case:
service.send('toggle');
Also, there is no need for isAuthenticated; this is redundant, since that state is represented by the finite state (state.value) of your machine.
I'm attempting to create a microservice based application that uses two remote Prisma/GraphQL schemas that run in Docker and a gateway that introspects them using schema stitching.
Prisma/GraphQL Schemas:
// Profile Schema service - http:localhost:3000/profile
type Profile {
id: ID!
user_id: ID!
firstName: String!
...
}
type Query {
findProfileById(id: ID!): Profile
findProfileByUserID(user_id: ID!): Profile
}
// User Schema service - http:localhost:5000/user
type User {
id: ID!
profileID: ID!
email: String!
...
}
type Query {
findUserById(id: ID!): User
findUserByProfileID(profileID: ID!): Profile
}
Now in the Gateway server I am able to introspect and mergeSchemas using graphql-tools successfully and I have added extended types to allow for relationships between the two types
// LinkTypeDefs
extend type Profile {
user: User
}
extend type User {
userProfile: Profile
}
I followed Apollo GraphQL documentation for schema stitching with remote schemas and this is the resolver I have for the now merged schemas
app.use('/gateway', bodyParser.json(), graphqlExpress({ schema: mergeSchemas({
schemas: [
profileSchema,
userSchema,
linkTypeDefs
],
resolvers: mergeInfo => ({
User: {
userProfile: {
fragment: `fragment UserFragment on User { id }`,
resolve(user, args, context, info) {
return delegateToSchema({
schema: profileSchema,
operation: 'query',
fieldName: 'findProfileByUserId',
args: {
user_id: user.id
},
context,
info
},
);
},
},
},
Profile: {
user: {
fragment: `fragment ProfileFragment on Profile { id }`,
resolve(profile, args, context, info) {
return delegateToSchema({
schema: authSchema,
operation: 'query',
fieldName: 'findUserByProfileId',
args: {
profileID: profile.id
},
context,
info
})
}
}
}
}),
})
}));
The issue I am having is everytime I query User or Profile for their new extended fields it always returns null. I have made sure that I have created User object with an existing profileId, likewise with Profile object with an existing userId. This is a sample of the query results
I have gone through the docs for about a week now and nothing seems to be working. From my undertstanding everything is plugged in properly. Hopefully someone can help. I have a feeling it has something to do with the fragments. I can provide a screenshot of the user and profile objects for more clarification if need be. Thanks.
I ended up resolving the issue by changing delegateToSchema() to info.mergeInfo.delegate(). That seemed to do the trick.
I have not tried the newly updated info.mergeInfo.delegateToSchema yet. Will update this post if the updated version works but for now this does the job for me.
Hopefully this can help someone in the future!