Merge pull request #797 from gradido/refresh-jwt

feat: New JWT in Every Authenticated Response
This commit is contained in:
Moriz Wahl 2021-09-13 20:33:48 +02:00 committed by GitHub
commit 3ef0b5c113
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 76 additions and 58 deletions

View File

@ -18,6 +18,7 @@
"apollo-server-express": "^2.25.2",
"axios": "^0.21.1",
"class-validator": "^0.13.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"graphql": "^15.5.1",

View File

@ -4,6 +4,7 @@ import { AuthChecker } from 'type-graphql'
import decode from '../jwt/decode'
import { apiGet } from '../apis/loginAPI'
import CONFIG from '../config'
import encode from '../jwt/encode'
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info }, roles) => {
@ -14,6 +15,7 @@ export const isAuthorized: AuthChecker<any> = async ({ root, args, context, info
`${CONFIG.LOGIN_API_URL}checkSessionState?session_id=${decoded.sessionId}`,
)
context.sessionId = decoded.sessionId
context.setHeaders.push({ key: 'token', value: encode(decoded.sessionId) })
return result.success
}
}

View File

@ -4,10 +4,11 @@
import { Resolver, Query, Args, Arg, Authorized, Ctx } from 'type-graphql'
import CONFIG from '../../config'
import { CheckUsernameResponse } from '../models/CheckUsernameResponse'
import { User } from '../models/User'
import { LoginViaVerificationCode } from '../models/LoginViaVerificationCode'
import { SendPasswordResetEmailResponse } from '../models/SendPasswordResetEmailResponse'
import { UpdateUserInfosResponse } from '../models/UpdateUserInfosResponse'
import { User } from '../models/User'
import encode from '../../jwt/encode'
import {
ChangePasswordArgs,
CheckUsernameArgs,
@ -16,12 +17,11 @@ import {
UpdateUserInfosArgs,
} from '../inputs/LoginUserInput'
import { apiPost, apiGet } from '../../apis/loginAPI'
import encode from '../../jwt/encode'
@Resolver()
export class UserResolver {
@Query(() => String)
async login(@Args() { email, password }: UnsecureLoginArgs): Promise<string> {
@Query(() => User)
async login(@Args() { email, password }: UnsecureLoginArgs, @Ctx() context: any): Promise<User> {
email = email.trim().toLowerCase()
const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password })
@ -30,10 +30,9 @@ export class UserResolver {
throw new Error(result.data)
}
const data = result.data
const sessionId = data.session_id
delete data.session_id
return encode({ sessionId, user: new User(data.user) })
context.setHeaders.push({ key: 'token', value: encode(result.data.session_id) })
return new User(result.data.user)
}
@Query(() => LoginViaVerificationCode)

View File

@ -2,6 +2,7 @@
import 'reflect-metadata'
import express from 'express'
import cors from 'cors'
import { buildSchema } from 'type-graphql'
import { ApolloServer } from 'apollo-server-express'
import { RowDataPacket } from 'mysql2/promise'
@ -22,14 +23,15 @@ import { isAuthorized } from './auth/auth'
const DB_VERSION = '0001-init_db'
const context = (req: any) => {
const authorization = req.req.headers.authorization
const context = (args: any) => {
const authorization = args.req.headers.authorization
let token = null
if (authorization) {
token = req.req.headers.authorization.replace(/^Bearer /, '')
token = authorization.replace(/^Bearer /, '')
}
const context = {
token,
setHeaders: [],
}
return context
}
@ -61,8 +63,31 @@ async function main() {
// Express Server
const server = express()
const corsOptions = {
origin: '*',
exposedHeaders: ['token'],
}
server.use(cors(corsOptions))
const plugins = [
{
requestDidStart() {
return {
willSendResponse(requestContext: any) {
const { setHeaders = [] } = requestContext.context
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
requestContext.response.http.headers.append(key, value)
})
return requestContext
},
}
},
},
]
// Apollo Server
const apollo = new ApolloServer({ schema, playground, context })
const apollo = new ApolloServer({ schema, playground, context, plugins })
apollo.applyMiddleware({ app: server })
// Start Server

View File

@ -7,15 +7,12 @@ import CONFIG from '../config/'
export default (token: string): any => {
if (!token) return null
let sessionId = null
const email = null
try {
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
sessionId = decoded.sub
// email = decoded.email
return {
token,
sessionId,
email,
}
} catch (err) {
return null

View File

@ -5,13 +5,9 @@ import jwt from 'jsonwebtoken'
import CONFIG from '../config/'
// Generate an Access Token
export default function encode(data: any): string {
const { user, sessionId } = data
const { email, language, firstName, lastName } = user
const token = jwt.sign({ email, language, firstName, lastName, sessionId }, CONFIG.JWT_SECRET, {
export default function encode(sessionId: string): string {
const token = jwt.sign({ sessionId }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES_IN,
// issuer: CONFIG.GRAPHQL_URI,
// audience: CONFIG.CLIENT_URI,
subject: sessionId.toString(),
})
return token

View File

@ -72,7 +72,6 @@
"vue-good-table": "^2.21.3",
"vue-i18n": "^8.22.4",
"vue-jest": "^3.0.7",
"vue-jwt-decode": "^0.1.0",
"vue-loading-overlay": "^3.4.2",
"vue-moment": "^4.1.0",
"vue-qrcode": "^0.3.5",

View File

@ -2,7 +2,14 @@ import gql from 'graphql-tag'
export const login = gql`
query($email: String!, $password: String!) {
login(email: $email, password: $password)
login(email: $email, password: $password) {
email
username
firstName
lastName
language
description
}
}
`

View File

@ -20,7 +20,11 @@ const authLink = new ApolloLink((operation, forward) => {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
},
})
return forward(operation)
return forward(operation).map((response) => {
const newToken = operation.getContext().response.headers.get('token')
if (newToken) store.commit('token', newToken)
return response
})
})
const apolloClient = new ApolloClient({

View File

@ -1,7 +1,6 @@
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import VueJwtDecode from 'vue-jwt-decode'
Vue.use(Vuex)
@ -30,15 +29,13 @@ export const mutations = {
}
export const actions = {
login: ({ dispatch, commit }, token) => {
const decoded = VueJwtDecode.decode(token)
commit('token', token)
commit('email', decoded.email)
commit('language', decoded.language)
commit('username', decoded.username)
commit('firstName', decoded.firstName)
commit('lastName', decoded.lastName)
commit('description', decoded.description)
login: ({ dispatch, commit }, data) => {
commit('email', data.email)
commit('language', data.language)
commit('username', data.username)
commit('firstName', data.firstName)
commit('lastName', data.lastName)
commit('description', data.description)
},
logout: ({ commit, state }) => {
commit('token', null)

View File

@ -1,15 +1,4 @@
import { mutations, actions } from './store'
import VueJwtDecode from 'vue-jwt-decode'
jest.mock('vue-jwt-decode')
VueJwtDecode.decode.mockReturnValue({
email: 'user@example.org',
language: 'de',
username: 'peter',
firstName: 'Peter',
lastName: 'Lustig',
description: 'Nickelbrille',
})
const { language, email, token, username, firstName, lastName, description } = mutations
const { login, logout } = actions
@ -77,46 +66,48 @@ describe('Vuex store', () => {
describe('login', () => {
const commit = jest.fn()
const state = {}
const commitedData = 'token'
const commitedData = {
email: 'user@example.org',
language: 'de',
username: 'peter',
firstName: 'Peter',
lastName: 'Lustig',
description: 'Nickelbrille',
}
it('calls seven commits', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenCalledTimes(7)
})
it('commits token', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(1, 'token', 'token')
expect(commit).toHaveBeenCalledTimes(6)
})
it('commits email', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(2, 'email', 'user@example.org')
expect(commit).toHaveBeenNthCalledWith(1, 'email', 'user@example.org')
})
it('commits language', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(3, 'language', 'de')
expect(commit).toHaveBeenNthCalledWith(2, 'language', 'de')
})
it('commits username', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(4, 'username', 'peter')
expect(commit).toHaveBeenNthCalledWith(3, 'username', 'peter')
})
it('commits firstName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(5, 'firstName', 'Peter')
expect(commit).toHaveBeenNthCalledWith(4, 'firstName', 'Peter')
})
it('commits lastName', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(6, 'lastName', 'Lustig')
expect(commit).toHaveBeenNthCalledWith(5, 'lastName', 'Lustig')
})
it('commits description', () => {
login({ commit, state }, commitedData)
expect(commit).toHaveBeenNthCalledWith(7, 'description', 'Nickelbrille')
expect(commit).toHaveBeenNthCalledWith(6, 'description', 'Nickelbrille')
})
})