見出し画像

これでばっちり!Auth0のRoleとPermission。

前回、Auth0を使うとログインとJWTによるアクセス制御が簡単にできると言うことをご紹介しました。

Auth0、これだけじゃありません。

RoleとPermissonも簡単に実装できるんです。

Role?Permission?

まずは、このRoleとPermissionが何者なのかを理解しなければいけません。

簡単に言うと、誰に(Role)に何に対してどういうアクセスを許可するのか(Permisson)と言うことです。

分かりやすく(?)、お母さん、お父さん、こどもと財布の関係で説明します。

画像1


画像2

人に対してRoleを割り当て、財布に対するアクセスにPermissionを設定すると言うことになります。

これをアプリケーションに対して適用すると、人、人物、役割に対してRoleを割り当て、APIに対するアクセスにPermissionを設定すると言うことになります。

Auth0のRoleとPermission

Auth0では、RoleとPermissionは以下のようになっています。

Role

Roleは単独で設定し、ユーザに割り当てます。

Roleに対し、APIごとに設定したPermissionを割り当てることができます。

つまり、ユーザにRoleが割り当てられた時点で、APIにどのようにアクセスできるかが決まります。

Permission

Permissionは、APIに対して設定します。

「何」に「どういう権限」を与えるかがPermissionですので、「権限:対象」とすると分かりやすいです。

例えば、memberに対して読み取り権限を与える場合は、「read:member」という感じで定義すると分かりやすいですね。

Auth0での設定

Auth0でRoleとPermissionを設定します。

Roleの設定

Roleの設定は、Users&RolesのRolesで行います。

画像3

Permissionの設定

APIにPermissionを設定します。

画像4

RoleにAPIのPermissionを設定

RoleにAPIのPermissionを設定します。

これで、ユーザにRoleを設定することで、APIのPermissionを設定することができます。

画像5

ユーザにRoleを割り当て

ユーザにロールを割り当てます。

画像6

こうすると、Roleに設定したAPIのPermissionがユーザに割り当てられます。

画像7

APIの設定

Auth0でログインしたときに、RoleとPermissionを扱えるようにするために、APIのRBACの設定を有効にします。

画像8

Add Permissions in the Access Tokenは、EnabledにするとJWTにユーザに割り当てられているPermissionが付加されます。

JWTは証明書がなくても簡単に中を読み取ることができますので、PermissionをJWTに含めるかは慎重に検討してください(JWTの作成、JWTの検証には証明書が必要なので簡単に偽装はできませんが)

Angularアプリケーションの実装

ユーザに割り当てられたPermissionによって、表示できるページをコントロールできるようします。

@auth0/angular-jwtのインストール

JWTをデコードするために、@auth0/angular-jwtパッケージを使います。

npm install @auth0/angular-jwt --save

実装

まずは、Permissionの定義をPermissions.tsに実装します。

export const Permissions = {
   // 契約:読み取り
   READ_CONTRACT: 'read:contract',
   // メンバー:読み取り
   READ_MEMBER: 'read:member',
   // メンバー:作成
   CREATE_MEMBER: 'create:member',
   // メンバー:更新
   UPDATE_MEMBER: 'update:member',
   // メンバー:削除
   DELETE_MEMBER: 'delete:member',
} as const;
export type Permission = typeof Permissions[keyof typeof Permissions];

次にユーザが指定したPermissionを持っているかをチェックするためのサービスクラスを作成します。

import { Injectable } from '@angular/core';
import { JwtHelperService } from '@auth0/angular-jwt';
import { Observable } from 'rxjs';

import { Permission } from './permissions';
import { AuthService } from '../auth/auth.service';

@Injectable({
   providedIn: 'root'
})
export class PermissionsService {
   
   constructor(
       private readonly authService: AuthService,
   ) { }
       
   /**
   * ログインしているユーザがロールを持っているかを取得する
   * @param roles 期待するロール
   */
   has(permissions: Permission | Permission[]): Observable<boolean> {
       return new Observable<boolean>((observer) => {
           // ユーザプロファイルを取得する
           this.authService.getToken$()
           .subscribe(
               t => {
                   const jwtHelperService: JwtHelperService = new JwtHelperService();
                   const token: any = jwtHelperService.decodeToken(t);
                   // トークンにpermissionsがなければfalseにする
                   if (!token || !token.permissions) {
                       observer.next(false);
                   }
                   // ユーザがロールを持っているかをチェックする
                   let ret;
                   if (typeof permissions !== 'string') {
                       ret = permissions.every(permission => token.permissions.includes(permission));
                   }
                   else {
                       ret = token.permissions.includes(permissions);
                   }
                   observer.next(ret);
               },
               err => {
                   observer.error(err);
               },
           );
       });
   }
}

最後に、Guardを実装します。

import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { PermissionsService } from './permissions.service';
import { Permission } from './permissions';

@Injectable({
 providedIn: 'root'
})
export class PermissionsGuard implements CanActivate {
 constructor(
   private readonly router: Router,
   private readonly permissionsService: PermissionsService
 ) {}

 canActivate(
   next: ActivatedRouteSnapshot,
   state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
   // routerのdataで設定されているrolesを取得する
   const roles: Permission[] = next.data.roles;

   const has = this.permissionsService.has(roles);

   if (!has) {
     this.router.navigateByUrl('/');
   }

   return has;
 }
}

これで、ルーティングの設定でGuardを以下のように使えば、ユーザに割り当てられたPermissionにあったアクセス制御を行うことができます。

{ path: 'member', component: MemberComponent, canActivate: [AuthGuard, PermissionsGuard], data: {roles: [Permissions.READ_MEMBER]} },

NestJS APIの実装

NestJS APIで、ユーザに割り当てられたPermissionでアクセスをコントールします。

※ここで紹介するのはGraphQL APIです。

まず、Permissionをpermissions.tsに定義します。

先ほどのAngularアプリケーションと同じです。

export const Permissions = {
   // 契約:読み取り
   READ_CONTRACT: 'read:contract',
   // メンバー:読み取り
   READ_MEMBER: 'read:member',
   // メンバー:作成
   CREATE_MEMBER: 'create:member',
   // メンバー:更新
   UPDATE_MEMBER: 'update:member',
   // メンバー:削除
   DELETE_MEMBER: 'delete:member',
} as const;
export type Permission = typeof Permissions[keyof typeof Permissions];

次にユーザに割り当てられているPermissionを持っているかをチェックするGuardを作成します。

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import { Permission } from './permissions';

@Injectable()
export class PermissionsGuard implements CanActivate {
   constructor(private readonly reflector: Reflector) {}
   
   canActivate(
       context: ExecutionContext,
   ): boolean | Promise<boolean> | Observable<boolean> {
       // APIに設定している権限を取得する
       const routePermissions = this.reflector.get<Permission[]>(
           'permission',
           context.getHandler(),
       );
           
       // APIに権限が設定されていなければOK
       if (!routePermissions) {
           return true;
       }

       // GraphQLのコンテキストからRequestを取得する
       const ctx = GqlExecutionContext.create(context);
       const req = ctx.getContext().req;

       // Request.user.permissionsがなければNG
       if (!req.user
           || !req.user.permissions) {
           return false;
       }
   
       return routePermissions.every(permission => req.user.permissions.includes(permission));
   }
}

最後にデコレータの定義をします。

import { SetMetadata } from '@nestjs/common';
import { Permission } from './permissions';

export const PermissionsHas = (...permissions: Permission[]) =>
 SetMetadata('permissions', permissions);

これで、Resolverクラスにデコレータを使ってGuardを設定すると、ユーザに割り当てられたPermissionにあったアクセス制御を行うことができます。

    @UseGuards(GraphqlAuthGuard, PermissionsGuard)
   @PermissionsHas(Permissions.CREATE_MEMBER)
   @Mutation(returns => MemberResult)
   async createMember(@Args({name: 'memberInput', type: () => MemberInput}) memberInput: MemberInput): Promise<MemberResult> {

Auth0のRoleとPermissionは、ダッシュボードを眺めていても仕組みを理解するのは難しいように思います。

実際に構築するアプリケーションでは、アクセス制御をしっかり行うことがとても重要です。

是非、Auth0のRoleとPermissionを攻略してみてください。


この記事が気に入ったらサポートをしてみませんか?