1
0

More than 1 year has passed since last update.

GraphQL(TypeGraphQL) における認可の実装例

Last updated at Posted at 2022-11-18

概要

Apollo ServerTypeGraphQL を使用して GraphQL API サーバを構築しました.
その中で, 下記の通り認可の実装をする必要がありました.

  • Administrator は全てのリソースに対してアクセスできること
  • Owner は自己所有のリソースに対してのみアクセスできること
    • 自己所有でないリソースにアクセスできないこと

サンプルコードを通して, TypeGraphQL における認可の実装例を紹介します.

サンプルコード

全体像

.
├── src
│   ├── authorizations
│   │   ├── authChecker.ts
│   │   ├── Role.ts
│   │   └── RolePolicy.ts
│   ├── schemas
│   │   ├── models
│   │   │   └── Users.ts
│   │   └── resolvers
│   │       ├── crud
│   │       │   ├── FindUniqueUsersArgs.ts
│   │       │   └── FindUniqueUsersResolver.ts
│   │       └── inputs
│   │           └── UsersWhereUniqueInput.ts
│   ├── context.ts
│   ├── index.ts
│   └── schema.ts
├── package.json
└── tsconfig.json

package.json

{
  ...
  "scripts": {
    "start": "ts-node src/index.ts"
  },
  "dependencies": {
    "apollo-server": "^3.11.1",
    "class-validator": "^0.13.2",
    "graphql": "^15.8.0",
    "jwt-decode": "^3.1.2",
    "reflect-metadata": "^0.1.13",
    "type-graphql": "^1.1.1"
  },
  "devDependencies": {
    "@types/node": "^18.11.9",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.3"
  }
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "lib": ["es2020"],
    "outDir": "./dist",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  }
}

src/context.ts

Apollo Server に渡す context を実装します.

src/context.ts
import { ContextFunction } from "apollo-server-core";
import { ExpressContext } from "apollo-server-express";
import jwtDecode from "jwt-decode";

// 今回使用する JWT クレームの型定義
type ContextIdentifyClaims = {
    sub: string;
    "custom:user_id": number;
    "custom:role": "Administrator" | "Owner";
    iat: number;
}

type ContextIdentity = {
    claims: ContextIdentifyClaims;
}

export type Context = {
    identity: ContextIdentity;
}

export const context: ContextFunction<ExpressContext> = ({ req }): Context => {
    const jwt = req.headers.authorization;

    return {
        identity: { claims: jwtDecode<ContextIdentifyClaims>(jwt ? jwt : "") },
    }
};

src/schemas

スキーマを定義していきましょう.

src/schemas/models/Users.ts
import { ObjectType, Field, Int } from "type-graphql";

@ObjectType()
export class Users {
    @Field(type => Int, { nullable: false })
    id!: number;

    @Field(type => String, { nullable: false })
    name!: string;
}
src/schemas/resolvers/crud/FindUniqueUsersArgs.ts
import { ArgsType, Field } from "type-graphql";
import { UsersWhereUniqueInput } from "../inputs/UsersWhereUniqueInput";

@ArgsType()
export class FindUniqueUsersArgs {
    @Field(type => UsersWhereUniqueInput, { nullable: false })
    where!: UsersWhereUniqueInput;
}
src/schemas/resolvers/crud/FindUniqueUsersResolver.ts
import { Resolver, Authorized, Query, Args } from "type-graphql";
import { Users } from "../../models/Users";
import { Role, AdministratorRole, OwnerRole } from "../../../authorizations/Role";
import { AdministratorAccess, UsersOwnerAccess } from "../../../authorizations/RolePolicy";
import { FindUniqueUsersArgs } from "./FindUniqueUsersArgs";

@Resolver(of => Users)
export class FindUniqueUsersResolver {
    private readonly users: Users[] = createUserSamples();

    // src/authorizations を実装したら追加してください
    @Authorized<Role>([
        { name: AdministratorRole.name, policy: AdministratorAccess },
        { name: OwnerRole.name, policy: UsersOwnerAccess }
    ])
    @Query(returns => Users, { nullable: true })
    async FindUniqueUsers(@Args() args: FindUniqueUsersArgs): Promise<Users | undefined> {
        return this.users.find(user => user.id === args.where.id);
    }
}

const createUserSamples = () => {
    return [
        createUser({
            id: 1,
            name: "Taro"
        }),
        createUser({
            id: 2,
            name: "Hanako",
        }),
    ];
};

const createUser = (userData: Partial<Users>) => {
    return Object.assign(new Users(), userData);
};

src/authorizations

認可の実装をしていきましょう.

src/authorizations/RolePolicy.ts
import { Context } from "../context";
import { ResolverData } from "type-graphql";

export type RolePolicy<TContextType = Context> = (
    resolverData: ResolverData<TContextType>,
) => boolean | Promise<boolean>;

export const AdministratorAccess: RolePolicy<Context> = () => {
    // 全てのリソースに対してアクセスを許可する
    return true;
};

export const UsersOwnerAccess: RolePolicy<Context> = ({ context, args }) => {
    // 自己所有のリソースに対してのみアクセスを許可する
    return context.identity.claims["custom:user_id"] === args.where.id;
};
src/authorizations/Role.ts
import { RolePolicy } from "./RolePolicy";

export type RoleName = "Administrator" | "Owner";

export type Role = {
    name: RoleName;
    policy: RolePolicy;
};

export const AdministratorRole: {
    name: RoleName;
} = {
    name: "Administrator",
};

export const OwnerRole: {
    name: RoleName;
} = {
    name: "Owner",
};
src/authorizations/authChecker.ts
import { AuthChecker } from "type-graphql";
import { Context } from "../context";
import { Role } from "./Role";

export const authChecker: AuthChecker<Context, Role> = ({ root, args, context, info }, roles: Role[]) => {
    // @Authorized() に渡されたロールを決定する
    const role = roles.find((role) => context.identity.claims["custom:role"].includes(role.name));

    // 決定されたロールのポリシーを満たせば true を返却する
    return role.policy({ root, args, context, info });
};

src/schema.ts

スキーマ構築時に, FindUniqueUsersResolver と authChecker を登録しましょう.

src/schema.ts
import { buildSchemaSync } from "type-graphql";
import { FindUniqueUsersResolver } from "./schemas/resolvers/crud/FindUniqueUsersResolver";
import { authChecker } from "./authorizations/authChecker";

export const schema = buildSchemaSync({
    resolvers: [FindUniqueUsersResolver],
    authChecker: authChecker,
});

src/index.ts

最後に, ApolloServer に schema と context を渡してあげましょう.

src/index.ts
import "reflect-metadata";
import { ApolloServer } from "apollo-server";
import { schema } from "./schema";
import { context } from "./context";

const server = new ApolloServer({
    schema,
    context,
});

server.listen().then(({ url }) => {
    console.log(`🚀  Server ready at ${url}`);
});

サーバ起動

$ node -v
v16.18.1
$ npm -v 
8.19.2
$ npm start
...
🚀  Server ready at http://localhost:4000/

テスト

Administrator ロール

JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY3VzdG9tOnVzZXJfaWQiOjEsImN1c3RvbTpyb2xlIjoiQWRtaW5pc3RyYXRvciIsImlhdCI6MTUxNjIzOTAyMn0.aP1H0BnN0VyhXUmdo2umcqsja_epigEcgSSEhGibGQM
payload
{
  "sub": "1234567890",
  "custom:user_id": 1,
  "custom:role": "Administrator",
  "iat": 1516239022
}

この JWT を持つユーザは Administrator ロールのため,
全てのリソース [{ id: 1, name: "Taro" }, { id: 2, name: "Hanako" } ] に対してアクセスができます.

実行結果

$ curl --request POST \
    --header 'content-type: application/json' \
    --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY3VzdG9tOnVzZXJfaWQiOjEsImN1c3RvbTpyb2xlIjoiQWRtaW5pc3RyYXRvciIsImlhdCI6MTUxNjIzOTAyMn0.aP1H0BnN0VyhXUmdo2umcqsja_epigEcgSSEhGibGQM' \
    --url http://localhost:4000/ \
    --data '{"query":"query FindUniqueUsers($where: UsersWhereUniqueInput!) {\n  FindUniqueUsers(where: $where) {\n    id\n    name\n  }\n}","variables":{"where":{"id":1}}}'
{"data":{"FindUniqueUsers":{"id":1,"name":"Taro"}}}
$ curl --request POST \
    --header 'content-type: application/json' \
    --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY3VzdG9tOnVzZXJfaWQiOjEsImN1c3RvbTpyb2xlIjoiQWRtaW5pc3RyYXRvciIsImlhdCI6MTUxNjIzOTAyMn0.aP1H0BnN0VyhXUmdo2umcqsja_epigEcgSSEhGibGQM' \
    --url http://localhost:4000/ \
    --data '{"query":"query FindUniqueUsers($where: UsersWhereUniqueInput!) {\n  FindUniqueUsers(where: $where) {\n    id\n    name\n  }\n}","variables":{"where":{"id":2}}}'
{"data":{"FindUniqueUsers":{"id":2,"name":"Hanako"}}}

いずれのリソースも無事取得できていますね.

Owner ロール

JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY3VzdG9tOnVzZXJfaWQiOjIsImN1c3RvbTpyb2xlIjoiT3duZXIiLCJpYXQiOjE1MTYyMzkwMjJ9.RcbMkhvM-4FOno0XkmJjeXCc-tBpcKJcakLO5XZaEUE
payload
{
  "sub": "1234567890",
  "custom:user_id": 2,
  "custom:role": "Owner",
  "iat": 1516239022
}

この JWT を持つユーザは custom:user_id が 2 で Owner ロールのため,
自己所有のリソース { id: 2, name: "Hanako" } にアクセスできますが,
自己所有でないリソース { id: 1, name: "Taro" } にはアクセスできません.

実行結果

$ curl --request POST \
    --header 'content-type: application/json' \
    --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY3VzdG9tOnVzZXJfaWQiOjIsImN1c3RvbTpyb2xlIjoiT3duZXIiLCJpYXQiOjE1MTYyMzkwMjJ9.RcbMkhvM-4FOno0XkmJjeXCc-tBpcKJcakLO5XZaEUE' \
    --url http://localhost:4000/ \
    --data '{"query":"query FindUniqueUsers($where: UsersWhereUniqueInput!) {\n  FindUniqueUsers(where: $where) {\n    id\n    name\n  }\n}","variables":{"where":{"id":2}}}'
{"data":{"FindUniqueUsers":{"id":2,"name":"Hanako"}}}
$ curl --request POST \
    --header 'content-type: application/json' \
    --header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiY3VzdG9tOnVzZXJfaWQiOjIsImN1c3RvbTpyb2xlIjoiT3duZXIiLCJpYXQiOjE1MTYyMzkwMjJ9.RcbMkhvM-4FOno0XkmJjeXCc-tBpcKJcakLO5XZaEUE' \
    --url http://localhost:4000/ \
    --data '{"query":"query FindUniqueUsers($where: UsersWhereUniqueInput!) {\n  FindUniqueUsers(where: $where) {\n    id\n    name\n  }\n}","variables":{"where":{"id":1}}}'
{"errors":[{"message":"Access denied! You don't have permission for this action!","locations":[{"line":2,"column":3}],"path":["FindUniqueUsers"],"extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace":["Error: Access denied! You don't have permission for this action!","    at /.../node_modules/type-graphql/dist/helpers/auth-middleware.js:13:79","    at processTicksAndRejections (node:internal/process/task_queues:96:5)","    at async dispatchHandler (/.../node_modules/type-graphql/dist/resolvers/helpers.js:82:24)"]}}}],"data":{"FindUniqueUsers":null}}

{ id: 2, name: "Hanako" } は取得できましたが, { id: 1, name: "Taro" } は取得できませんでした.
いずれも期待通りの振る舞いですね.

最後に

TypeGraphQL における認可の実装例を紹介しました.
サンプルコードのため, JWT の verify をしていなかったり, エラーハンドリングを省いていたりするのでご注意ください.

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0