Nextjs getServerSideProps does not pass session to component - session

Good morning. I am using NextJS and get the session in the getServerSideProps block.
I am passing multiple parameters to the return object, however, the session does not pass. All of the other key value pairs works, but when I try to log the session, It comes undefined... I don't understand...
const DashboardPage = ({ session, secret }) => {
const [loading, setloading] = useState(false);
//This does not work
console.log('Session: ', session);
return (
<section className="border-4 border-orange-800 max-w-5xl mx-auto">
<CreateListModal userId={session?.userId} loading={loading} setloading={setloading} />
</section>
)
}
export const getServerSideProps = async context => {
// get sessions with added user Id in the session object
const session = await requireAuthentication(context);
// This works
console.log(session);
if (!session) {
return {
redirect: {
destination: '/signup',
permanent: false
}
}
}
else {
return {
props: {
session: session,
secret: 'Pasarika este dulce'
}
}
}
}
export default DashboardPage;
The purpose of the requireAuthentication function is to create a new session object where I will insert another key value pair that will contain the user id and the session that will be used in the entire app.
In that function I get the user by email that is returned by the session and get the Id from the db. I than return the new session object that looks like this:
{
user: {
name: 'daniel sas',
email: 'email#gmail.com',
image: 'https://lh3.googldeusercontent.com/a/A6r44ZwMyONqcfJORNnuYtbVv_LYbab-wv5Uyxk=s96-c',
userId: 'clbcpc0hi0002sb1wsiea3q5d'//This is the required thing in my app
},
expires: '2022-12-23T08:04:08.263Z'
}
The following function is used to get the data from the database
import { getSession } from "next-auth/react";
import prisma from './prisma'
// This function get the email and returns a new session object that includes
// the userId
export const requireAuthentication = async context => {
const session = await getSession(context);
// If there is no user or there is an error ret to signup page
if (!session) return null
// If the user is not found return same redirect to signup
else {
try {
const user = await prisma.user.findUnique({where: { email: session.user.email }});
if (!user) return null;
// Must return a new session here that contains the userId...
else {
const newSession = {
user: {
...session.user,
userId: user.id
},
expires: session.expires
};
return newSession;
}
}
catch (error) {
if (error) {
console.log(error);
}
}
}
}

Related

How do I refresh information in NextAuth session?

I am currently playing around and building SaaS web application with NextJS, NextAuth, MongoDB and Stripe. My issue is when I change subscribtion of the current user in the database it isn't reflected in the session and I have to logout and login again to see the changes.
import { compare } from 'bcryptjs';
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import connectMongo from '../../../database/connection';
import Users from '../../../models/Schema';
export default NextAuth({
providers: [
CredentialsProvider({
name: 'Credentials',
async authorize(credentials, req) {
connectMongo().catch((error) => {
error: 'Connection Failed...!';
});
// check user existance
const result = await Users.findOne({ email: credentials.email });
if (!result) {
throw new Error('No user Found with Email Please Sign Up...!');
}
// compare()
const checkPassword = await compare(
credentials.password,
result.password
);
// incorrect password
if (!checkPassword || result.email !== credentials.email) {
throw new Error('Username or Password does not match');
}
return result;
},
}),
],
session: {
jwt: true,
},
jwt: {
secret: process.env.JWT_SECRET,
},
callbacks: {
async jwt({token, user}) {
if(user) {
token.stripeCustomerId = user.stripeCustomerId;
token.subscribtionRole = user.subscribtionRole;
}
return token;
},
async session({session, token}) {
session.user.stripeCustomerId = token.stripeCustomerId;
session.user.subscribtionRole = token.subscribtionRole;
return session;
}
}
});
I've tried searching the documentation and various articles, but I haven't found anything that helps. I'm new to this and just learning, so there are a few things I don't understand and don't have that much knowledge, so any advice would be appreciated.

How to mutate user's session in nextauth when you change user data?

I want to update the user's data but after updating the user's data how to make also the change appear in session?
[...nextauth].js
callbacks: {
jwt: ({ token, user }) => {
if (user) {
token.id = user.id;
token.name = user.name;
token.surname = user.surname;
token.email = user.email;
token.role = user.role;
}
// Here, check the token validity date
if (token.tokenExpiration < Date.now()) {
// Call the endpoint where you handle the token refresh for a user
const user = axios.post(
`${process.env.API_URL}/auth/authentication/refresh`,
{
refreshToken: token.refreshToken,
}
);
// Check for the result and update the data accordingly
return { ...token, ...user };
}
return token;
},
session: ({ session, token }) => {
if (token) {
session.id = token.id;
session.name = token.name;
session.surname = token.surname;
session.email = token.email;
session.role = token.role;
}
return session;
},
},
secret: process.env.SECRET_KEY,
jwt: {
secret: process.env.SECRET_KEY,
encryption: true,
maxAge: 5 * 60 * 1000,
},
api/user/index.js
Here I update the user content, what should I do to update the session object detail
const updateUser = await prisma.user.update({
where: {
email: 'test#email.io',
},
data: {
name: 'User',
},
})
session object
name : Company
email : test#email.io
expires : 2022-04-26T18:44:36.424Z
id : 2
name : Company
surname : Surname
email : test#email.io
role : 2
I have the same problem and have a hacky workaround. In the session callback, get the user from the database. This callback is triggered whenever the session is checked (docs), so you can call getSession(), useSession(), or even signIn() somewhere after you update the user.
async function getUserFromDB(accessToken) {
// I'm not super familiar with prisma but you get the idea
const user = await prisma.user.findUnique({
// logic to get user
// maybe it needs your accessToken
});
return user;
}
// [...nextAuth].js
session: ({ session, token }) => {
if (token) {
session = await getUserFromDB(token.accessToken);
}
return session;
}
Obvious drawback: There is a call to get the user from the database anytime the session is checked.

Supabase third party oAuth providers are returning null?

I'm trying to implement Facebook, Google and Twitter authentication. So far, I've set up the apps within the respective developer platforms, added those keys/secrets to my Supabase console, and created this graphql resolver:
/* eslint-disable #typescript-eslint/explicit-module-boundary-types */
import camelcaseKeys from 'camelcase-keys';
import { supabase } from 'lib/supabaseClient';
import { LoginInput, Provider } from 'generated/types';
import { Provider as SupabaseProvider } from '#supabase/supabase-js';
import Context from '../../context';
import { User } from '#supabase/supabase-js';
export default async function login(
_: any,
{ input }: { input: LoginInput },
{ res, req }: Context
): Promise<any> {
const { provider } = input;
// base level error object
const errorObject = {
__typename: 'AuthError',
};
// return error object if no provider is given
if (!provider) {
return {
...errorObject,
message: 'Must include provider',
};
}
try {
const { user, session, error } = await supabase.auth.signIn({
// provider can be 'github', 'google', 'gitlab', or 'bitbucket'
provider: 'facebook',
});
console.log({ user });
console.log({ session });
console.log({ error });
if (error) {
return {
...errorObject,
message: error.message,
};
}
const response = camelcaseKeys(user as User, { deep: true });
return {
__typename: 'LoginSuccess',
accessToken: session?.access_token,
refreshToken: session?.refresh_token,
...response,
};
} catch (error) {
return {
...errorObject,
message: error.message,
};
}
}
I have three console logs set up directly underneath the signIn() function, all of which are returning null.
I can also go directly to https://<your-ref>.supabase.co/auth/v1/authorize?provider=<provider> and auth works correctly, so it appears to have been narrowed down specifically to the signIn() function. What would cause the response to return null values?
This is happening because these values are not populated until after the redirect from the OAuth server takes place. If you look at the internal code of supabase/gotrue-js you'll see null being returned explicitly.
private _handleProviderSignIn(
provider: Provider,
options: {
redirectTo?: string
scopes?: string
} = {}
) {
const url: string = this.api.getUrlForProvider(provider, {
redirectTo: options.redirectTo,
scopes: options.scopes,
})
try {
// try to open on the browser
if (isBrowser()) {
window.location.href = url
}
return { provider, url, data: null, session: null, user: null, error: null }
} catch (error) {
// fallback to returning the URL
if (!!url) return { provider, url, data: null, session: null, user: null, error: null }
return { data: null, user: null, session: null, error }
}
}
The flow is something like this:
Call `supabase.auth.signIn({ provider: 'github' })
User is sent to Github.com where they will be prompted to allow/deny your app access to their data
If they allow your app access, Github.com redirects back to your app
Now, through some Supabase magic, you will have access to the session, user, etc. data

Call redux action in axios interceptor

I store logged in user data in localstorage. I also validate JWT token in axios interceptor and if it's expired I will refresh it. so I need to update store with new user data and JWT token and in order to do that, I need to call redux action that I have in my Auth module.
AuthRedux.js
export const actionTypes = {
Login: "[Login] Action",
Logout: "[Logout] Action",
UserRequested: "[Request User] Action",
UserLoaded: "[Load User] Auth API",
SetUser: "[Set User] Action",
};
const initialAuthState = {
user: undefined,
authToken: undefined,
};
export const reducer = persistReducer(
{ storage, key: "myapp-auth", whitelist: ["user", "authToken"] },
(state = initialAuthState, action) => {
switch (action.type) {
case actionTypes.Login: {
const { authToken } = action.payload;
return { authToken, user: undefined };
}
case actionTypes.Logout: {
// TODO: Change this code. Actions in reducer aren't allowed.
return initialAuthState;
}
case actionTypes.UserLoaded: {
const { user } = action.payload;
return { ...state, user };
}
case actionTypes.SetUser: {
const { user } = action.payload;
return { ...state, user };
}
default:
return state;
}
}
);
export const actions = {
login: (authToken) => ({ type: actionTypes.Login, payload: { authToken } }),
logout: () => ({ type: actionTypes.Logout }),
requestUser: (user) => ({ type: actionTypes.UserRequested, payload: { user } }),
fulfillUser: (user) => ({ type: actionTypes.UserLoaded, payload: { user } }),
setUser: (user) => ({ type: actionTypes.SetUser, payload: { user } }),
};
export function* saga() {
yield takeLatest(actionTypes.Login, function* loginSaga() {
yield put(actions.requestUser());
});
yield takeLatest(actionTypes.UserRequested, function* userRequested() {
const { data: user } = yield getUserByToken();
yield put(actions.fulfillUser(user));
});
}
AxiosInterceptor.js
export default function setupAxios(axios, store, props) {
axios.interceptors.request.use(
config => {
const {
auth: { authToken }
} = store.getState();
if (authToken) {
config.headers.Authorization = `Bearer ${authToken}`;
}
return config;
},
err => Promise.reject(err)
);
axios.interceptors.response.use(
(response) => {
console.log(props);
return response;
},
function (error) {
const originalRequest = error.config;
if (error.response?.status === 401) {
if (error.response.data === "refresh_token") {
// refresh token and set new user data
// question is how can I call redux setUser action in here and update state
}
else if (error.response.data === "invalid_token") {
window.localStorage.clear();
window.location.href = '/auth/login';
}
else { }
}
if (!originalRequest._retry) {
originalRequest._retry = true;
return axios(originalRequest);
}
return Promise.reject(error);
}
);
}
My question is how can I call reducer setUser action in interceptor and update state
You can dispatch actions from outside of a component when you have access to the store with store.dispatch(anAction), in your case you can do:
store.dispatch(setUser())

How to set Cognito Groups in Migration trigger

I am currently building a migration solution from an AWS Userpool to another using the CognitoTrigger "User Migration".
I have a Group I want to set during migration but I cannot do it because the user isn't created before the whole context finishes.
How can I solve this? I don't want to create a PostAuth - lambda because I only need/want/can run this once per migration and I also want to do this the instant (or up to a few minutes later) the migration happens. (or is it possible to make this PostAuth check if it is the first time it triggers?)
I tried PostConfirm in the hopes of this triggering when the user was created but that did not trigger.
If someone else runs into this - I solved this using a combination of a User Migration trigger and a Pre Token Generation trigger.
In the User Migration trigger (mostly copied from https://github.com/Collaborne/migrate-cognito-user-pool-lambda) look up and create the user if auth fails/user doesn't exist in the new pool.
In the Pre Token Generation trigger if the user hasn't been added to groups yet look up group membership in the old user pool (adminListGroupsForUser), add them to the new pool (adminAddUserToGroup). The crucial part is to override the group membership claims in the response so that they will be added to the token on the client side (groupsToOverride is just an array of the group names the user is part of):
event.response = {
"claimsOverrideDetails": {
"claimsToAddOrOverride": {
},
"groupOverrideDetails": {
"groupsToOverride": groupsToOverride,
}
}
};
Thank you #BrokenGlass, I used this approach. For anyone else here's an example Typescript preTokenGeneration lambda.
//preTokenGenerations.ts
import { PreTokenGenerationTriggerHandler } from 'aws-lambda';
import { preTokenAuthentication } from '../services/preTokenService';
export const handler: PreTokenGenerationTriggerHandler = async (event, context) => {
console.log({
event,
context,
request: event.request,
userAttributes: event.request.userAttributes,
clientMetadata: event.request.clientMetadata,
groupConfiguration: event.request.groupConfiguration,
})
const OLD_USER_POOL_ID = process.env.OLD_USER_POOL_ID;
if (!OLD_USER_POOL_ID) {
throw new Error("OLD_USER_POOL_ID is required for the lambda to work.")
}
const {
userPoolId,
request: {
userAttributes: {
email
}
},
region
} = event;
switch (event.triggerSource) {
case "TokenGeneration_Authentication":
const groupsToOverride = await preTokenAuthentication({
userPoolId,
oldUserPoolId: OLD_USER_POOL_ID,
username: email,
region
})
event.response = {
"claimsOverrideDetails": {
"claimsToAddOrOverride": {
},
"groupOverrideDetails": {
"groupsToOverride": groupsToOverride,
}
}
};
return event
default:
console.log(`Bad triggerSource ${event.triggerSource}`);
return new Promise((resolve) => {
resolve(event)
});
}
}
// preTokenService.ts
import { getUsersGroups, cognitoIdentityServiceProvider, assignUserToGroup } from "./cognito"
interface IPreTokenAuthentication {
userPoolId: string;
oldUserPoolId: string;
username: string;
region: string
}
export const preTokenAuthentication = async ({ userPoolId, oldUserPoolId, username, region }: IPreTokenAuthentication): string[] => {
const cognitoISP = cognitoIdentityServiceProvider({ region });
const newPoolUsersGroups = await getUsersGroups({
cognitoISP,
userPoolId,
username
});
// If the user in the new pool already has groups assigned then exit
if (newPoolUsersGroups.length !== 0) {
console.log("No action required user already exists in a group")
return;
}
const oldPoolUsersGroups = await getUsersGroups({
cognitoISP,
userPoolId: oldUserPoolId,
username
});
// If the user in the old pool doesn't have any groups then nothing else for this function to do so exit.
if (oldPoolUsersGroups.length === 0) {
console.error("No action required user migrated user didn't belong to a group")
return;
}
console.log({ oldPoolUsersGroups, newPoolUsersGroups })
await assignUserToGroup({
cognitoISP,
userPoolId,
username,
groups: oldPoolUsersGroups
})
return oldPoolUsersGroups;
}
// cognito.ts
import { AdminAddUserToGroupRequest, AdminListGroupsForUserRequest } from "aws-sdk/clients/cognitoidentityserviceprovider";
import { CognitoIdentityServiceProvider } from 'aws-sdk';
interface ICognitoIdentityServiceProvider {
region: string;
}
export const cognitoIdentityServiceProvider = ({ region }: ICognitoIdentityServiceProvider) => {
const options: CognitoIdentityServiceProvider.Types.ClientConfiguration = {
region,
};
const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider(options);
return cognitoIdentityServiceProvider;
}
interface IGetUsersGroups {
cognitoISP: CognitoIdentityServiceProvider,
userPoolId: string,
username: string
}
export const getUsersGroups = async ({ cognitoISP, userPoolId, username }: IGetUsersGroups): Promise<string[]> => {
try {
const params: AdminListGroupsForUserRequest = {
UserPoolId: userPoolId,
Username: username,
}
const response = await cognitoISP.adminListGroupsForUser(params).promise();
return response.Groups?.map(group => group.GroupName!) || [];
} catch (err) {
console.error(err)
return [];
}
}
interface IAssignUserToGroup {
cognitoISP: CognitoIdentityServiceProvider,
username: string;
groups: string[];
userPoolId: string;
}
/**
* Use Administration to assign a user to groups
* #param {
* cognitoISP the cognito identity service provider to perform the action on
* userPoolId the userPool for which the user is being modified within
* username the username or email for which the action is to be performed
* groups the groups to assign the user too
* }
*/
export const assignUserToGroup = async ({ cognitoISP, userPoolId, username, groups }: IAssignUserToGroup) => {
console.log({ userPoolId, username, groups })
for (const group of groups) {
const params: AdminAddUserToGroupRequest = {
UserPoolId: userPoolId,
Username: username,
GroupName: group
};
try {
const response = await cognitoISP.adminAddUserToGroup(params).promise();
console.log({ response })
} catch (err) {
console.error(err)
}
}
}
Tips, make sure under the trigger section in Cognito that you have the migration and preToken triggers set. You also need to ensure SRP is not enabled so the lambda can see the password to be able to successfully migrate the user.
Things to test is that when the user is first migrated that they are assigned their groups. And for future logins they are also assigned to their groups.
Let me know if anyone has any feedback or questions, happy to help.

Resources