これでばっちり!Auth0のRoleとPermission。
前回、Auth0を使うとログインとJWTによるアクセス制御が簡単にできると言うことをご紹介しました。
Auth0、これだけじゃありません。
RoleとPermissonも簡単に実装できるんです。
Role?Permission?
まずは、このRoleとPermissionが何者なのかを理解しなければいけません。
簡単に言うと、誰に(Role)に何に対してどういうアクセスを許可するのか(Permisson)と言うことです。
分かりやすく(?)、お母さん、お父さん、こどもと財布の関係で説明します。
人に対して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を攻略してみてください。