I'm using nuxt 3 with #sidebase/nuxt-auth for my PKCE OAUTH2 authentication flow with my Laravel API which uses Laravel passport. I am done with the implementation of the authentication flow from the Laravel side, and done with the nuxt side, getting the token and saving it.
Then I have created a custom interceptor with ofetch, in order to send the access_token that I fetch from the session at every request.
but when the access token is expired, it is not getting refreshed automatically.
am i missing some configuration? or refresh token is a custom logic i have to write?
I've tried the JWT callback in the #sidebase/nuxt-auth but it didn't work.
This is my current auth configuration:
import {NuxtAuthHandler} from '#auth'
import useCustomFetch from "~/composables/useCustomFetch";
export default NuxtAuthHandler({
providers: [
{
id: 'passport',
name: 'Passport',
type: 'oauth',
version: '2.0',
authorization: {
url: "https://example.com/oauth/authorize",
params: {
scope: '',
prompt: 'front',
},
},
clientSecret: 'awd',
clientId: "96695f40-1578-4b7c-974b-181e0344dcac",
token: 'https://example.com/api/v1/oauth/token',
userinfo: 'https://example.com/api/v1/user',
checks: ['pkce'],
profile(profile: { success: { user: any } }) {
const data = profile.success.user
return {
id: data.id,
name: data.first_name + " " + data.last_name,
email: data.email,
};
},
}
],
cookies: {
},
callbacks: {
async jwt({token, account, user}) {
if (account && user) {
return {
access_token: account.access_token,
refresh_token: account.refresh_token,
accessTokenExpires: account.expires_at,
user
}
}
// #ts-ignore
if (Date.now() < token.accessTokenExpires * 1000) {
return token
}
return await refreshAccessToken(token);
},
async session({session, token}) {
// #ts-ignore
session.user = token.user
// #ts-ignore
session.access_token = token.access_token
// #ts-ignore
session.error = token.error
return session
},
},
events: {
async signOut() {
try {
await useCustomFetch('/oauth/tokens/revoke', {
method: 'POST'
})
} catch (e) {
console.log(e);
}
},
}
})
async function refreshAccessToken(token: any) {
try {
const url = "https://example.com/api/v1/oauth/token";
// #ts-ignore
const refreshedToken: AuthResponse = await $fetch(url, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: {
grant_type: 'refresh_token',
refresh_token: token.refresh_token,
client_id: "96695f40-1578-4b7c-974b-181e0344dcac"
}
});
token.access_token = refreshedToken.access_token;
token.accessTokenExpires = Date.now() + refreshedToken.expires_at * 1000;
token.refresh_token = refreshedToken.refresh_token;
return {
...token
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
interface AuthResponse {
access_token: string,
refresh_token: string,
token_type: string,
expires_at: number,
}
Related
I'm trying to use NextAuth and Strapi in my app, but NextAuth session only shows email for the user.
When I call the Strapi login API directly:
axios
.post(`${process.env.STRAPI_URL}/api/auth/local`, {
identifier: "email#provider.com",
password: "test123",
})
.then((response) => {
console.log("User profile", response.data.user);
})
I get this object (response.data.user) in console:
{
"id": 4,
"username": "theusername",
"email": "email#provider.com",
"provider": "local",
"confirmed": true,
"blocked": false,
"createdAt": "2022-09-18T17:02:43.581Z",
"updatedAt": "2022-09-27T16:39:22.993Z"
}
But when I try to sign in with NextAuth:
import axios from "axios";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
// Configure one or more authentication providers
providers: [
CredentialsProvider({
name: "Sign in with Email",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
// return (
if (credentials == null) return null;
try {
/* I tried `...rest` but no luck. also console log doesn't work here and I can't see the actual response from the API call */
const { user, jwt } =
(await axios
.post(
`${process.env.STRAPI_URL}/api/auth/local`,
{
identifier: credentials.email,
password: credentials.password,
}
)
.then((response) => {
return response.data;
})
.catch((error) => {
console.log(error.response);
throw new Error(error.response.data.message);
})) || null;
return { jwt, ...user };
} catch (error) {
console.warn(error);
// Sign In Fail
// return null;
}
// );
},
}),
],
callbacks: {
session: async ({ session, token }) => {
session.id = token.id;
session.jwt = token.jwt;
return Promise.resolve(session);
},
jwt: async ({ token, user }) => {
if (user) {
token.id = user.id;
token.jwt = user.jwt;
}
return Promise.resolve(token);
},
},
});
I only get email for the user:
{
"user": {
"email": "email#provider.com"
},
"expires": "2022-10-27T20:03:20.177Z",
"id": 4,
"jwt": "somejwtcodethatichangedhereforsecurity"
}
How can I have other properties, like username, in the user returned from NextAuth?
It's possible, by modifying callbacks:
callbacks: {
session: async ({ session, token }) => {
session.id = token.id;
session.jwt = token.jwt;
session.user.username = token.username /* added */
return Promise.resolve(session);
},
jwt: async ({ token, user }) => {
if (user) {
token.id = user.id;
token.jwt = user.jwt;
token.username = user.username /* added */
}
return Promise.resolve(token);
},
},
I am making authentication with SvelteKit and Laravel. This is the flow i currently have:
User logs in with correct credentials.
User login route has no middleware enabled on the Laravel side.
This login request returns a JWT token, which gets send back to the Sveltekit server.
I set this token as a cookie using this code:
const headers = {
'Set-Cookie': cookie.serialize(variables.authCookieName, body.token, {
path: '/',
httpOnly: true,
sameSite: 'lax'
})
}
return {
headers,
body: {
user
}
}
The cookie is correctly set after that, verified.
So the authentication is handled correctly. But now i want to send that cookie with Axios to the Laravel server and authenticate the user but that doesn't work. The Laravel server never receives the cookie. The Axios withCredentials setting also never sends that cookie to the Laravel server. How can i make it work so that the cookie header is sent with Axios to Laravel? I have 0 CORS errors in my browser so i don't think that is the issue.
My API Class in SvelteKit:
import axios from 'axios'
import { variables } from '$lib/variables'
const headers: Record<string, string | number | boolean> = {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
class Api {
constructor() {
axios.defaults.baseURL = variables.apiUrl
axios.defaults.withCredentials = true
axios.interceptors.response.use(
response => response.data,
error => Promise.reject(error.response.data)
)
}
get(url: string) {
return axios.get(url, { headers })
}
post(url: string, data?: unknown) {
return axios.post(url, data, { headers })
}
patch(url: string, data: Record<string, unknown>) {
return axios.patch(url, data, { headers })
}
}
const api = new Api()
export default api
My Userservice:
import api from '$core/api'
const resource = '/users'
const userService = () => {
const getAll = async () => {
return await api.get(resource)
}
return {
getAll
}
}
export default userService
The Index endpoint (routes/dashboard/index.ts)
import services from '$core/services'
export async function get() {
return await services.user.getAll()
.then(({ data }) => {
return {
body: { users: data.users }
}
}).catch((err) => {
return {
body: { error: err.message }
}
})
}
My Hooks.index.ts (maybe for reference)
import * as cookie from 'cookie'
import jwt_decode from 'jwt-decode'
import type { GetSession, Handle } from '#sveltejs/kit'
import type { User } from '$interfaces/User'
// This is server side
/** #type {import('#sveltejs/kit').Handle} */
export const handle: Handle = async ({ event, resolve }) => {
const { jwt } = cookie.parse(event.request.headers.get('cookie') || '')
if (jwt) {
const { user } = jwt_decode<{ user: User }>(jwt)
if (user) {
event.locals.user = user
}
}
return resolve(event)
}
export const getSession: GetSession = async (request) => {
return {
user: request.locals.user
}
}
Can someone help or explain why Axios has no idea if the cookie is set or not, or how i can send the Cookie with the request to the Laravel Server?
I want to add custom header in the response of one of my GraphQL resolver Queries in Nestjs. here is the code.
#Query(() => LoginUnion)
async login(
#Args({ type: () => LoginArgs }) { LoginInfo: { email, password } } : LoginArgs
): Promise<typeof LoginUnion> {
try {
let userInfo = await this.userModel.findOne({ email, password: SHA256(password).toString() }, { 'password': false });
if (userInfo === null) {
return new CustomErrorResponse({ message: 'Wrong password or username'});
}
if (!userInfo.isVerified) {
return new CustomErrorResponse({ message: 'User in not verified'});
}
const token = await this.authentication.sign(userInfo, config.get('Secure_JWT_Sign_Key'));
// set header here
console.log('_DEBUG_ =>', token);
} catch(error) {
console.log('user.resolver.login => ', error);
return new CustomErrorResponse({ message: 'Server Exception', additonalInfo: JSON.stringify(error) });
}
}
as per the title, I am having problem trying to enable graphql subscription in my loopback 4 application.
Here is my code that I've done so far.
index.ts
export async function main(options: ApplicationConfig = {}) {
const app = new BackendLb4Application(options)
await app.boot()
await app.start()
const url = app.restServer.url;
const oas: Oas3 = <Oas3><unknown>await app.restServer.getApiSpec()
const {schema} = await createGraphQLSchema(oas, {
operationIdFieldNames: true,
baseUrl: url,
createSubscriptionsFromCallbacks: true,
})
const handler = graphqlHTTP( (request:any, response:any, graphQLParams: any) => ({
schema,
pretty: true,
graphiql: true
}))
app.mountExpressRouter(graphqlPath, handler);
const pubsub = new PubSub()
const ws = createServer(app);
ws.listen(PORT, () => {
new SubscriptionServer(
{
execute,
subscribe,
schema,
onConnect: (params: any, socket: any, ctx: any) => {
console.log(params, 'here on onconnect')
// Add pubsub to context to be used by GraphQL subscribe field
return { pubsub }
}
},
{
server: ws,
path: '/subscriptions'
}
)
})
return app
}
Here is my schema
type Subscription {
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlApiNotification(secondInputInput: SecondInputInput): String
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlOnNotificationUpdate(firstInputInput: FirstInputInput): String
}
Here is an example of my controller
#patch('/notification-update', {
operationId: 'notificationUpdate',
description: '**GraphQL notificationUpdate**',
callbacks:[ {
onNotificationUpdate: {
//'{$request.query.callbackUrl}/onNotificationUpdate': {
post: {
requestBody: {
operationId: 'notificationUpdateCallback',
description: 'rasjad',
content: {
'application/json': {
schema: {
title: "firstInput",
type: 'object',
properties: {
userData: {
type: "string"
}
}
}
}
}
},
responses: {
'200': {
description: 'response to subscription',
}
}
}
},
// }
}],
responses: {
'200': {
description: 'Notification PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Notification, {partial: true}),
},
},
})
notification: Notification,
#param.where(Notification) where?: Where<Notification>,
): Promise<Count> {
return this.notificationRepository.update(notification, where);
}
Ive defined the callbacks object in my controller which will then create a subscription in my schema. Tested it out on graphiql but did not work.
I am not sure where to go from here. Do I need a custom resolver or something? Not sure.
Appreciate it if anyone could help on this.
Just in case someone else is looking to do the same thing.
I switched out graphqlHTTP with Apollo Server to create my graphql server.
So my final index.ts looks like this.
export async function main(options: ApplicationConfig = {}) {
const lb4Application = new BackendLb4Application(options)
await lb4Application.boot()
await lb4Application.migrateSchema()
await lb4Application.start()
const url = lb4Application.restServer.url;
const graphqlPath = '/graphql'
// Get the OpenApiSpec
const oas: Oas3 = <Oas3><unknown>await lb4Application.restServer.getApiSpec()
// Create GraphQl Schema from OpenApiSpec
const {schema} = await createGraphQLSchema(oas, {
strict: false,
viewer: true,
baseUrl: url,
headers: {
'X-Origin': 'GraphQL'
},
createSubscriptionsFromCallbacks: true,
customResolvers: {
"lb4-title": {
"your-path":{
patch: (obj, args, context, info) => {
const num = Math.floor(Math.random() * 10);
pubsub.publish("something", { yourMethodName: {count: num} }).catch((err: any) => {
console.log(err)
})
return {count: 1}
}
}
}
},
customSubscriptionResolvers: {
"lb4-title" : {
"yourMethodName": {
post: {
subscribe: () => pubsub.asyncIterator("something"),
resolve: (obj: any, args: any, context, info) => {
console.log(obj, 'obj')
}
}
}
}
}
})
const app = express();
const server = new ApolloServer({
schema,
plugins: [{
async serverWillStart() {
return {
async drainServer() {
subscriptionServers.close();
}
};
}
}],
})
const subscriptionServers = SubscriptionServer.create(
{
// This is the `schema` we just created.
schema,
// These are imported from `graphql`.
execute,
subscribe,
},
{
server: lb4Application.restServer.httpServer?.server,
path: server.graphqlPath,
//path: server.graphqlPath,
}
);
await server.start();
server.applyMiddleware({ app, path: "/" });
lb4Application.mountExpressRouter('/graphql', app);
return lb4Application
}
Also you will need to define the callbacks object in your controller like so.
#patch('/something-update', {
operationId: 'somethingUpdate',
description: '**GraphQL somethingUpdate**',
callbacks:[
{
yourMethodName: {
post: {
responses: {
'200': {
description: 'response to subscription',
content: {'application/json': {schema: CountSchema}},
}
}
}
},
}
],
responses: {
'200': {
description: 'Something PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Something, {partial: true}),
},
},
})
something: Something,
#param.where(Something) where?: Where<Something>,
): Promise<Count> {
return this.somethingRepository.updateAll(something, where);
}
And that is it. You can test it out from the GraphQL Playground and play around with the subscriptions.
For the time being, I am fine with defining customResolvers and customSubscriptionResolvers but I'm pretty sure I can automate this two objects from the controllers.
Cheers!
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())