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

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

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

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

Role?Permission?

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

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

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

RolePermission
お母さん財布にお金を入れる
財布からお金を出す
お父さん財布にお金を入れる
財布からはお金を出せない(T_T)
こども財布にはアクセスできない

人に対して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で行います。

Permissionの設定

APIにPermissionを設定します。

RoleにAPIのPermissionを設定

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

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

ユーザにRoleを割り当て

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

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

APIの設定

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

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を攻略してみてください。