فهرست منبع

feat(server): Convert auth to GraphQL, fix auth guard mechanism

Closes #15
Michael Bromley 7 سال پیش
والد
کامیت
c03735f474

+ 1 - 1
.prettierignore

@@ -1 +1 @@
-gql-generated-types.ts
+generated-types.ts

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
schema.json


+ 2 - 2
server/src/api/api.module.ts

@@ -6,7 +6,7 @@ import { I18nModule } from '../i18n/i18n.module';
 import { ServiceModule } from '../service/service.module';
 
 import { AdministratorResolver } from './administrator/administrator.resolver';
-import { AuthController } from './auth/auth.controller';
+import { AuthResolver } from './auth/auth.resolver';
 import { ConfigResolver } from './config/config.resolver';
 import { CustomerResolver } from './customer/customer.resolver';
 import { FacetResolver } from './facet/facet.resolver';
@@ -17,6 +17,7 @@ import { ProductResolver } from './product/product.resolver';
 
 const exportedProviders = [
     AdministratorResolver,
+    AuthResolver,
     ConfigResolver,
     FacetResolver,
     CustomerResolver,
@@ -37,7 +38,6 @@ const exportedProviders = [
             imports: [ConfigModule, I18nModule],
         }),
     ],
-    controllers: [AuthController],
     providers: [...exportedProviders, JwtStrategy],
     exports: exportedProviders,
 })

+ 64 - 0
server/src/api/auth-guard.ts

@@ -0,0 +1,64 @@
+import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+import * as passport from 'passport';
+import { ExtractJwt, Strategy } from 'passport-jwt';
+import { Observable } from 'rxjs';
+
+import { JwtPayload } from '../common/types/auth-types';
+import { ConfigService } from '../config/config.service';
+import { AuthService } from '../service/auth.service';
+
+/**
+ * A guard which uses passport.js & the passport-jwt strategy to authenticate incoming GraphQL requests.
+ * At this time, the Nest AuthGuard is not working with Apollo Server 2, see https://github.com/nestjs/graphql/issues/48
+ *
+ * If the above issue is fixed, it may make sense to switch to the Nest AuthGuard.
+ */
+@Injectable()
+export class AuthGuard implements CanActivate {
+    strategy: any;
+
+    constructor(private configService: ConfigService, private authService: AuthService) {
+        this.strategy = new Strategy(
+            {
+                secretOrKey: configService.jwtSecret,
+                jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
+            },
+            (payload: any, done: () => void) => this.validate(payload, done),
+        );
+    }
+
+    async canActivate(context: ExecutionContext): Promise<boolean> {
+        const ctx = GqlExecutionContext.create(context).getContext();
+        return this.authenticate(ctx.req);
+    }
+
+    async validate(payload: JwtPayload, done: (err: Error | null, user: any) => void) {
+        const user = await this.authService.validateUser(payload);
+        if (!user) {
+            return done(new UnauthorizedException(), false);
+        }
+        done(null, user);
+    }
+
+    /**
+     * Wraps the JwtStrategy.authenticate() call in a Promise, and also patches
+     * the methods which it expects to exist because it is designed to run as
+     * an Express middleware function.
+     */
+    private authenticate(request: any): Promise<boolean> {
+        return new Promise((resolve, reject) => {
+            this.strategy.fail = info => {
+                resolve(false);
+            };
+            this.strategy.success = (user, info) => {
+                request.user = user;
+                resolve(true);
+            };
+            this.strategy.error = err => {
+                reject(err);
+            };
+            this.strategy.authenticate(request);
+        });
+    }
+}

+ 19 - 0
server/src/api/auth/auth.api.graphql

@@ -0,0 +1,19 @@
+type Query {
+    me: CurrentUser
+}
+
+type Mutation {
+    login(username: String!, password: String!): LoginResult!
+}
+
+type LoginResult {
+    authToken: String!
+    user: CurrentUser!
+}
+
+type CurrentUser {
+    id: ID!
+    identifier: String!
+    roles: [String!]!
+    channelTokens: [String!]!
+}

+ 18 - 12
server/src/api/auth/auth.controller.ts → server/src/api/auth/auth.resolver.ts

@@ -1,25 +1,26 @@
-import { Body, Controller, Get, Post, Req } from '@nestjs/common';
+import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 
 import { Role } from '../../common/types/role';
 import { User } from '../../entity/user/user.entity';
 import { AuthService } from '../../service/auth.service';
+import { ChannelService } from '../../service/channel.service';
 import { RolesGuard } from '../roles-guard';
 
-@Controller('auth')
-export class AuthController {
-    constructor(private authService: AuthService) {}
+@Resolver('Auth')
+export class AuthResolver {
+    constructor(private authService: AuthService, private channelService: ChannelService) {}
 
     /**
      * Attempts a login given the username and password of a user. If successful, returns
      * the user data and a token to be used by Bearer auth.
      */
-    @Post('login')
-    async login(@Body() loginDto: { username: string; password: string }) {
-        const { user, token } = await this.authService.createToken(loginDto.username, loginDto.password);
+    @Mutation()
+    async login(@Args() args: { username: string; password: string }) {
+        const { user, token } = await this.authService.createToken(args.username, args.password);
 
         if (token) {
             return {
-                token,
+                authToken: token,
                 user: this.publiclyAccessibleUser(user),
             };
         }
@@ -29,20 +30,25 @@ export class AuthController {
      * Returns information about the current authenticated user.
      */
     @RolesGuard([Role.Authenticated])
-    @Get('me')
-    async me(@Req() request) {
+    @Query()
+    me(@Context('req') request: any) {
         const user = request.user as User;
-        return this.publiclyAccessibleUser(user);
+        return user ? this.publiclyAccessibleUser(user) : null;
     }
 
     /**
      * Exposes a subset of the User properties which we want to expose to the public API.
      */
-    private publiclyAccessibleUser(user: User): Pick<User, 'id' | 'identifier' | 'roles'> {
+    private publiclyAccessibleUser(
+        user: User,
+    ): Pick<User, 'id' | 'identifier' | 'roles'> & { channelTokens: string[] } {
         return {
             id: user.id,
             identifier: user.identifier,
             roles: user.roles,
+            channelTokens: user.roles.includes(Role.SuperAdmin)
+                ? [this.channelService.getDefaultChannel().token]
+                : [],
         };
     }
 }

+ 3 - 0
server/src/api/jwt.strategy.ts

@@ -7,6 +7,9 @@ import { AuthService } from '../service/auth.service';
 
 import { JwtPayload } from '../common/types/auth-types';
 
+/**
+ * Currently unused - see note at {@link AuthGuard}
+ */
 @Injectable()
 export class JwtStrategy extends PassportStrategy(Strategy) {
     constructor(private readonly authService: AuthService, private configService: ConfigService) {

+ 3 - 3
server/src/api/roles-guard.ts

@@ -1,10 +1,10 @@
 import { CanActivate, ExecutionContext, UseGuards } from '@nestjs/common';
-import { AuthGuard } from '@nestjs/passport';
 import { ExtractJwt, Strategy } from 'passport-jwt';
 
+import { Role } from '../common/types/role';
 import { User } from '../entity/user/user.entity';
 
-import { Role } from '../common/types/role';
+import { AuthGuard } from './auth-guard';
 
 /**
  * A guard which combines the JWT passport auth method with restrictions based on
@@ -21,7 +21,7 @@ import { Role } from '../common/types/role';
  */
 export function RolesGuard(roles: Role[]) {
     // tslint:disable-next-line:ban-types
-    const guards: Array<CanActivate | Function> = [AuthGuard('jwt')];
+    const guards: Array<CanActivate | Function> = [AuthGuard];
 
     if (roles.length && !authenticatedOnly(roles)) {
         guards.push(forRoles(roles));

+ 13 - 0
server/src/service/channel.service.ts

@@ -4,6 +4,7 @@ import { Connection } from 'typeorm';
 
 import { DEFAULT_CHANNEL_CODE } from '../common/constants';
 import { Channel } from '../entity/channel/channel.entity';
+import { I18nError } from '../i18n/i18n-error';
 
 @Injectable()
 export class ChannelService {
@@ -27,6 +28,18 @@ export class ChannelService {
         return this.allChannels.find(channel => channel.token === token);
     }
 
+    /**
+     * Returns the default Channel.
+     */
+    getDefaultChannel(): Channel {
+        const defaultChannel = this.allChannels.find(channel => channel.code === DEFAULT_CHANNEL_CODE);
+
+        if (!defaultChannel) {
+            throw new I18nError(`error.default-channel-not-found`);
+        }
+        return defaultChannel;
+    }
+
     findAll(): Promise<Channel[]> {
         return this.connection.getRepository(Channel).find();
     }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است