Browse Source

feat: Implement configurable cookie/bearer session token methods

Relates to #25.
Michael Bromley 7 years ago
parent
commit
ff10e896f4

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 1 - 0
server/e2e/auth.e2e-spec.ts

@@ -54,6 +54,7 @@ describe('Authorization & permissions', () => {
             await assertRequestAllowed<AttemptLoginVariables>(ATTEMPT_LOGIN, {
                 username: SUPER_ADMIN_USER_IDENTIFIER,
                 password: SUPER_ADMIN_USER_PASSWORD,
+                rememberMe: false,
             });
         });
     });

+ 1 - 0
server/e2e/config/test-config.ts

@@ -17,6 +17,7 @@ export const testConfig: VendureConfig = {
     cors: true,
     authOptions: {
         sessionSecret: 'some-secret',
+        tokenMethod: 'bearer',
     },
     dbConnectionOptions: {
         type: 'sqljs',

+ 23 - 13
server/mock-data/simple-graphql-client.ts

@@ -1,15 +1,11 @@
 import { DocumentNode } from 'graphql';
 import { GraphQLClient } from 'graphql-request';
+import gql from 'graphql-tag';
 import { print } from 'graphql/language/printer';
 import { Curl } from 'node-libcurl';
-import { AttemptLogin, AttemptLoginVariables, CreateAssets } from 'shared/generated-types';
-import {
-    AUTH_TOKEN_KEY,
-    SUPER_ADMIN_USER_IDENTIFIER,
-    SUPER_ADMIN_USER_PASSWORD,
-} from 'shared/shared-constants';
+import { CreateAssets } from 'shared/generated-types';
+import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from 'shared/shared-constants';
 
-import { ATTEMPT_LOGIN } from '../../admin-ui/src/app/data/definitions/auth-definitions';
 import { CREATE_ASSETS } from '../../admin-ui/src/app/data/definitions/product-definitions';
 import { getConfig } from '../src/config/vendure-config';
 
@@ -107,12 +103,26 @@ export class SimpleGraphQLClient {
     }
 
     async asUserWithCredentials(username: string, password: string) {
-        const result = await this.client.rawRequest<AttemptLogin>(print(ATTEMPT_LOGIN), {
-            username,
-            password,
-        } as AttemptLoginVariables);
-        if (result.data && result.data.login) {
-            this.setAuthToken(result.headers.get(AUTH_TOKEN_KEY));
+        const result = await this.query(
+            gql`
+                mutation($username: String!, $password: String!) {
+                    login(username: $username, password: $password) {
+                        user {
+                            id
+                            identifier
+                            channelTokens
+                        }
+                        token
+                    }
+                }
+            `,
+            {
+                username,
+                password,
+            },
+        );
+        if (result && result.login) {
+            this.setAuthToken(result.login.token);
         } else {
             console.error(result);
         }

+ 24 - 3
server/src/api/common/auth-guard.ts

@@ -1,6 +1,7 @@
 import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
 import { Reflector } from '@nestjs/core';
 import { GqlExecutionContext } from '@nestjs/graphql';
+import { Request } from 'express';
 
 import { ConfigService } from '../../config/config.service';
 import { AuthService } from '../../service/providers/auth.service';
@@ -27,11 +28,11 @@ export class AuthGuard implements CanActivate {
             return true;
         }
         const ctx = GqlExecutionContext.create(context).getContext();
-        const session = ctx.req.session;
-        if (!session || !session.token) {
+        const token = this.getToken(ctx.req);
+        if (!token) {
             return false;
         }
-        const activeSession = await this.authService.validateSession(session.token);
+        const activeSession = await this.authService.validateSession(token);
         if (activeSession) {
             ctx.req.user = activeSession.user;
             return true;
@@ -39,4 +40,24 @@ export class AuthGuard implements CanActivate {
             return false;
         }
     }
+
+    /**
+     * Get the session token from either the cookie or the Authorization header, depending
+     * on the configured tokenMethod.
+     */
+    private getToken(req: Request): string | undefined {
+        if (this.configService.authOptions.tokenMethod === 'cookie') {
+            if (req.session && req.session.token) {
+                return req.session.token;
+            }
+        } else {
+            const authHeader = req.get('Authorization');
+            if (authHeader) {
+                const matches = authHeader.match(/bearer\s+(.+)$/i);
+                if (matches) {
+                    return matches[1];
+                }
+            }
+        }
+    }
 }

+ 0 - 1
server/src/api/common/roles-guard.ts

@@ -1,6 +1,5 @@
 import { ExecutionContext, Injectable, ReflectMetadata } from '@nestjs/common';
 import { Reflector } from '@nestjs/core';
-import { ExtractJwt, Strategy } from 'passport-jwt';
 import { Permission } from 'shared/generated-types';
 
 import { idsAreEqual } from '../../common/utils';

+ 17 - 6
server/src/api/resolvers/auth.resolver.ts

@@ -3,6 +3,7 @@ import { Request } from 'express';
 import * as ms from 'ms';
 import { AttemptLoginVariables, Permission } from 'shared/generated-types';
 
+import { ConfigService } from '../../config/config.service';
 import { User } from '../../entity/user/user.entity';
 import { AuthService } from '../../service/providers/auth.service';
 import { ChannelService } from '../../service/providers/channel.service';
@@ -10,25 +11,35 @@ import { Allow } from '../common/roles-guard';
 
 @Resolver('Auth')
 export class AuthResolver {
-    constructor(private authService: AuthService, private channelService: ChannelService) {}
+    constructor(
+        private authService: AuthService,
+        private channelService: ChannelService,
+        private configService: ConfigService,
+    ) {}
 
     /**
      * Attempts a login given the username and password of a user. If successful, returns
-     * the user data and sets the session.
+     * the user data and returns the token either in a cookie or in the response body.
      */
     @Mutation()
     async login(@Args() args: AttemptLoginVariables, @Context('req') request: Request) {
         const session = await this.authService.authenticate(args.username, args.password);
 
         if (session) {
-            if (request.session) {
-                if (args.rememberMe) {
-                    request.sessionOptions.maxAge = ms('1y');
+            let token: string | null = null;
+            if (this.configService.authOptions.tokenMethod === 'cookie') {
+                if (request.session) {
+                    if (args.rememberMe) {
+                        request.sessionOptions.maxAge = ms('1y');
+                    }
+                    request.session.token = session.token;
                 }
-                request.session.token = session.token;
+            } else {
+                token = session.token;
             }
             return {
                 user: this.publiclyAccessibleUser(session.user),
+                token,
             };
         }
     }

+ 3 - 1
server/src/api/types/auth.api.graphql

@@ -3,12 +3,14 @@ type Query {
 }
 
 type Mutation {
-    login(username: String!, password: String!, rememberMe: Boolean!): LoginResult!
+    login(username: String!, password: String!, rememberMe: Boolean): LoginResult!
     logout: Boolean!
 }
 
 type LoginResult {
     user: CurrentUser!
+    "The token will only be returned when using the `bearer` tokenMethod"
+    token: String
 }
 
 type CurrentUser {

+ 5 - 3
server/src/app.module.ts

@@ -22,15 +22,17 @@ export class AppModule implements NestModule {
 
         const defaultMiddleware: Array<{ handler: RequestHandler; route?: string }> = [
             { handler: this.i18nService.handle(), route: this.configService.apiPath },
-            {
+        ];
+        if (this.configService.authOptions.tokenMethod === 'cookie') {
+            defaultMiddleware.push({
                 handler: cookieSession({
                     name: 'session',
                     secret: this.configService.authOptions.sessionSecret,
                     httpOnly: true,
                 }),
                 route: this.configService.apiPath,
-            },
-        ];
+            });
+        }
         const allMiddleware = defaultMiddleware.concat(this.configService.middleware);
         const middlewareByRoute = this.groupMiddlewareByRoute(allMiddleware);
         for (const [route, handlers] of Object.entries(middlewareByRoute)) {

+ 2 - 1
server/src/config/default-config.ts

@@ -23,7 +23,8 @@ export const defaultConfig: ReadOnlyRequired<VendureConfig> = {
     },
     authOptions: {
         disableAuth: false,
-        sessionSecret: 'jwt-secret',
+        tokenMethod: 'cookie',
+        sessionSecret: 'session-secret',
         sessionDuration: '7d',
     },
     apiPath: API_PATH,

+ 13 - 2
server/src/config/vendure-config.ts

@@ -22,14 +22,25 @@ export interface AuthOptions {
      */
     disableAuth?: boolean;
     /**
-     * The secret used for signing the session cookies for authenticated users.
+     * Sets the method by which the session token is delivered and read.
+     *
+     * - "cookie": Upon login, a 'Set-Cookie' header will be returned to the client, setting a
+     *   cookie containing the session token. A browser-based client (making requests with credentials)
+     *   should automatically send the session cookie with each request.
+     * - "bearer": Upon login, the token is returned in the response and should be then stored by the
+     *   client app. Each request should include the header "Authorization: Bearer <token>".
+     */
+    tokenMethod?: 'cookie' | 'bearer';
+    /**
+     * The secret used for signing the session cookies for authenticated users. Only applies when
+     * tokenMethod is set to "cookie".
      *
      * In production applications, this should not be stored as a string in
      * source control for security reasons, but may be loaded from an external
      * file not under source control, or from an environment variable, for example.
      * See https://stackoverflow.com/a/30090120/772859
      */
-    sessionSecret: string;
+    sessionSecret?: string;
     /**
      * Session duration, i.e. the time which must elapse from the last authenticted request
      * after which the user must re-authenticate.

+ 1 - 0
server/src/service/providers/auth.service.ts

@@ -33,6 +33,7 @@ export class AuthService {
             token,
             user,
             expires: new Date(Date.now() + ms(this.configService.authOptions.sessionDuration)),
+            invalidated: false,
         });
         await this.invalidateUserSessions(user);
         // save the new session

+ 16 - 2
shared/generated-types.ts

@@ -727,8 +727,15 @@ export interface RequestCompleted {
 // GraphQL mutation operation: SetAsLoggedIn
 // ====================================================
 
+export interface SetAsLoggedIn_setAsLoggedIn {
+  __typename: "UserStatus";
+  username: string;
+  isLoggedIn: boolean;
+  loginTime: string;
+}
+
 export interface SetAsLoggedIn {
-  setAsLoggedIn: boolean;
+  setAsLoggedIn: SetAsLoggedIn_setAsLoggedIn;
 }
 
 export interface SetAsLoggedInVariables {
@@ -743,8 +750,15 @@ export interface SetAsLoggedInVariables {
 // GraphQL mutation operation: SetAsLoggedOut
 // ====================================================
 
+export interface SetAsLoggedOut_setAsLoggedOut {
+  __typename: "UserStatus";
+  username: string;
+  isLoggedIn: boolean;
+  loginTime: string;
+}
+
 export interface SetAsLoggedOut {
-  setAsLoggedOut: boolean;
+  setAsLoggedOut: SetAsLoggedOut_setAsLoggedOut;
 }
 
 /* tslint:disable */

+ 0 - 1
shared/shared-constants.ts

@@ -4,7 +4,6 @@
  */
 export const API_PORT = 3000;
 export const API_PATH = 'api';
-export const AUTH_TOKEN_KEY = 'vendure-auth-token';
 export const DEFAULT_CHANNEL_CODE = '__default_channel__';
 export const SUPER_ADMIN_ROLE_CODE = '__super_admin_role__';
 export const SUPER_ADMIN_ROLE_DESCRIPTION = 'SuperAdmin';

Some files were not shown because too many files changed in this diff