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?
Related
I'm trying to add authentication in graphql, so only authenticated users can make a request to my server. But when I try to send the id in the Authorization header it doesn't send the information I want. It's a next project with ssr.
import { ApolloClient, InMemoryCache, createHttpLink } from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import { getAuth } from 'firebase/auth';
const httpLink = createHttpLink({
uri: 'http://localhost:4000/graphql',
});
const authLink = setContext((_, { headers }) => {
const user = getAuth().currentUser;
return {
headers: {
...headers,
authorization: user?.uid ?? "null"
}
}
})
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
export default client;
I have the id of the user in a redux context, so I tried to use store.getState(), but it didn't work. Also I've tried with firebase getAuth, so I send the uid.
I am using the default passport jwt AuthGuard for my project. That works for my post & get routes fine when setting the authentication header.
Now I want to use Nestjs Gateways as well with socket.io on the client-side, but I don't know how to send the access_token to the gateway?
That is basically my Gateway:
#WebSocketGateway()
export class UserGateway {
entityManager = getManager();
#UseGuards(AuthGuard('jwt'))
#SubscribeMessage('getUserList')
async handleMessage(client: any, payload: any) {
const results = await this.entityManager.find(UserEntity);
console.log(results);
return this.entityToClientUser(results);
}
And on the client I'm sending like this:
this.socket.emit('getUserList', users => {
console.log(users);
this.userListSub.next(users);
});
How and where do I add the jwt access_token? The documentation of nestjs misses that point completely for Websockets. All they say is, that the Guards work exactly the same for websockets as they do for post / get etc. See here
While the question is answered, I want to point out the Guard is not usable to prevent unauthorized users from establishing a connection.
It's only usable to guard specific events.
The handleConnection method of a class annotated with #WebSocketGateway is called before canActivate of your Guard.
I end up using something like this in my Gateway class:
async handleConnection(client: Socket) {
const payload = this.authService.verify(
client.handshake.headers.authorization,
);
const user = await this.usersService.findOne(payload.userId);
!user && client.disconnect();
}
For anyone looking for a solution. Here it is:
#UseGuards(WsGuard)
#SubscribeMessage('yourRoute')
async saveUser(socket: Socket, data: any) {
let auth_token = socket.handshake.headers.authorization;
// get the token itself without "Bearer"
auth_token = auth_token.split(' ')[1];
}
On the client side you add the authorization header like this:
this.socketOptions = {
transportOptions: {
polling: {
extraHeaders: {
Authorization: 'your token', // 'Bearer h93t4293t49jt34j9rferek...'
}
}
}
};
// ...
this.socket = io.connect('http://localhost:4200/', this.socketOptions);
// ...
Afterwards you have access to the token on every request serverside like in the example.
Here also the WsGuard I implemented.
#Injectable()
export class WsGuard implements CanActivate {
constructor(private userService: UserService) {
}
canActivate(
context: any,
): boolean | any | Promise<boolean | any> | Observable<boolean | any> {
const bearerToken = context.args[0].handshake.headers.authorization.split(' ')[1];
try {
const decoded = jwt.verify(bearerToken, jwtConstants.secret) as any;
return new Promise((resolve, reject) => {
return this.userService.findByUsername(decoded.username).then(user => {
if (user) {
resolve(user);
} else {
reject(false);
}
});
});
} catch (ex) {
console.log(ex);
return false;
}
}
}
I simply check if I can find a user with the username from the decoded token in my database with my user service. I am sure you could make this implementation cleaner, but it works.
Thanks! At the end i implemented a Guard that like the jwt guard puts the user inside the request. At the end I'm using the query string method from the socket client to pass the auth token This is my implementation:
import { CanActivate, ExecutionContext, Injectable, Logger } from '#nestjs/common';
import { WsException } from '#nestjs/websockets';
import { Socket } from 'socket.io';
import { AuthService } from '../auth/auth.service';
import { User } from '../auth/entity/user.entity';
#Injectable()
export class WsJwtGuard implements CanActivate {
private logger: Logger = new Logger(WsJwtGuard.name);
constructor(private authService: AuthService) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const client: Socket = context.switchToWs().getClient<Socket>();
const authToken: string = client.handshake?.query?.token;
const user: User = await this.authService.verifyUser(authToken);
client.join(`house_${user?.house?.id}`);
context.switchToHttp().getRequest().user = user
return Boolean(user);
} catch (err) {
throw new WsException(err.message);
}
}
}
I hooked up a front end to a graphql server. Most if not all the mutations are protected while all the queries are not protected. I have an auth system in place where if you log in, you get an access/refresh token which all mutations are required to use. And they do which is great, backend receives the headers and everything!
HOWEVER. There is one query that needs at least the access token to distinguish the current user! BUT the backend does not receive the two headers! I thought that the middlewareLink I created would be for all queries/mutations but I'm wrong and couldn't find any additional resources to help me out.
So here's my setup
apollo-client.js
import { InMemoryCache } from "apollo-cache-inmemory"
import { persistCache } from "apollo-cache-persist"
import { ApolloLink } from "apollo-link"
import { HttpLink } from "apollo-link-http"
import { onError } from "apollo-link-error"
import { setContext } from "apollo-link-context"
if (process.browser) {
try {
persistCache({
cache,
storage: window.localStorage
})
} catch (error) {
console.error("Error restoring Apollo cache", error)
}
}
const httpLink = new HttpLink({
uri: process.env.GRAPHQL_URL || "http://localhost:4000/graphql"
})
const authMiddlewareLink = setContext(() => ({
headers: {
authorization: localStorage.getItem("apollo-token") || null,
"x-refresh-token": localStorage.getItem("refresh-token") || null
}
}))
const afterwareLink = new ApolloLink((operation, forward) =>
forward(operation).map(response => {
const context = operation.getContext()
const {
response: { headers }
} = context
if (headers) {
const token = headers.get("authorization")
const refreshToken = headers.get("x-refresh-token")
if (token) {
localStorage.setItem("apollo-token", token)
}
if (refreshToken) {
localStorage.setItem("refresh-token", refreshToken)
}
}
return response
})
)
const errorLink = onError(({ graphQLErrors, networkError }) => {
...
// really long error link code
...
})
let links = [errorLink, afterwareLink, httpLink]
if (process.browser) {
links = [errorLink, afterwareLink, authMiddlewareLink, httpLink]
}
const link = ApolloLink.from(links)
export default function() {
return {
cache,
defaultHttpLink: false,
link
}
}
Is there a way to target ALL mutations/queries with custom headers not just mutations? Or apply some headers to an individual query since I could probably put that as an app middleware?
edit: Haven't solved the SSR portion of this yet.. will re-edit with the answer once I have.
I am new to Graphql and I am using the Apollo client with Angular 7.
I have a mutation in the server that I am using for authentication.This mutation generates returns an access token and a refresh token:
#Injectable({
providedIn: "root"
})
export class LoginTokenAuthGQL extends Apollo.Mutation<
LoginTokenAuth.Mutation,
LoginTokenAuth.Variables
> {
document: any = gql`
mutation loginTokenAuth($input: LoginAuthInput!) {
loginTokenAuth(input: $input) {
accessToken
refreshToken
}
}
`;
}
I am running this mutation in my sign-in component like this:
onLoginSubmit() {
const email = this.loginForm.controls['userEmail'].value;
const password = this.loginForm.controls['userPassword'].value;
console.log('Sending mutation with', email, password);
this.loginGQL.mutate({
input: {
email,
password,
userType: AuthUserType.Crm
}
}).pipe(
map((response) => response.data )
).subscribe(
(output: LoginTokenAuth.Mutation) => {
console.log('Access token', output.loginTokenAuth.accessToken);
console.log('Refresh token', output.loginTokenAuth.refreshToken);
console.log(this.apollo.getClient().cache);
},
((error: any) => {
console.error(error);
})
);
}
Once I get the access token I will need to add it as header on my requests.
From what I read from the Apollo Client all results from queries and mutations are cached locally in the client. But it is not clear to me how can I access them and add it to the apollo-link.
To be more clear I would like to do this in my Graphql module:
const http = httpLink.create({uri: '/graphql'});
const auth = setContext((_, { headers }) => {
// get the authentication token from the cache
const token = ???
if (!token) {
return {};
} else {
return {
headers: headers.append('Authorization', `Bearer ${token}`)
};
}
});
Even official docs of Apollo Client suggest you store this token as usually - to localStorage.
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
const httpLink = createHttpLink({
uri: '/graphql',
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});
I have my build running on my localhost. I have the dataProvider working properly with all standard views using https (i.e. "Show", Create"). I want to create data from within a function like this:
import { CREATE, GET_ONE, UPDATE } from 'react-admin';
import dataProviderFactory from '../dataProvider';
dataProviderFactory(
process.env.REACT_APP_SERVER_HTTPS_URL
).then(dataProvider => {
dataProvider(CREATE, 'batches', {
data: {
name: "test"
}
})
.then(response => response.data)
.then(data => {
console.log('data', data);
});
});
The exact same POST in the entity 'batches' works from the standard react-admin "create" template, but with this function, it fails. It appears to be trying to proxy to 'localhost' instead of maintaining the root API URL. Is there some other way I need to either pull the dataProvider from the Admin controller, or can I spec the proxy root url for the dataProvider?
dataProvier.js:
export default type => {
return import('./rest').then(provider => provider.default);
};
rest.js:
import simpleRestProvider from 'ra-data-simple-rest';
import Constants from "../constants/Constants";
import { fetchUtils } from 'react-admin';
import SimpleRestClient from '../utils/SimpleRestClient';
import AppConfig from '../AppConfig';
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers();
}
if(!url.endsWith("/authenticate") && localStorage.getItem('token') !== null) {
options.user = {
authenticated: true,
token: 'Bearer ' + localStorage.getItem('token')
}
}
return fetchUtils.fetchJson(url, options);
}
// Use custom rest client. For fakeserver use react admin rest client
const restDataProvider = !AppConfig.useFakeServer ? SimpleRestClient(Constants.urls.apiBaseUrl, httpClient) :
simpleRestProvider(Constants.urls.apiBaseUrl, httpClient);
export default (type, resource, params) =>
new Promise(resolve => {
if (!AppConfig.useFakeServer) {
setTimeout(() => resolve(restDataProvider(type, resource, params)), 500);
} else {
resolve(restDataProvider(type, resource, params));
}
}
);