GAE と Angular で普通の SPA の作り方 〜 認可編

# はじめに

* [前回](https://qiita.com/nirasan/items/85b635ec2ccb580643a6)で OAuth 認証を使ってユーザー認証と登録をするところまでできた。
* 今回は認証成功時にアプリ専用のアクセストークンを発行し、アクセストークンで Angular から GAE へのリクエストを認可する処理を実装する。
  * OAuth 認証時に OAuth プロバイダのアクセストークンを取得することはできるが、これを流用すると OAuth プロバイダの認証された情報へのアクセス経路を無駄に公開することになるので、アプリ専用のアクセストークンを別途発行して利用する。

# コード

* この記事を終えたコードは[こちら](https://github.com/nirasan/gae-angular-sample/tree/qiita.com/nirasan/items/2543c62e16c6b7bd88e3)。

# OAuth 認証成功時にアプリ専用のアクセストークンを発行する

* OAuth コールバックハンドラでアプリ専用のアクセストークンを発行し、Cookie 経由でクライアントに渡す。
* アクセストークンは JWT を利用し、JWT 署名には HMAC-SHA512 アルゴリズムを利用する。
* HMAC-SHA512 の暗号化には 128bit の秘密鍵が必要になるので、秘密鍵を環境変数から取得できるようにして GAE 設定ファイルに定義する。

```go:server/app/handler.go
func OauthCallbackHandler(e echo.Context) error {
	
	// ... 略 ...
	
	// アクセストークンを作成して Cookie でわたす
	token := jwt.NewWithClaims(jwt.GetSigningMethod("HS512"), jwt.MapClaims{
		"sub": userinfo.Sub,
		"exp": time.Now().Add(time.Hour * 24).Unix(),
	})
	hmackey, err := GetHMACKey()
	if err != nil {
		return err
	}
	log.Debugf(ctx, "HMAC_KEY: %s", hmackey.String())
	signedToken, err := token.SignedString(hmackey.Bytes())
	if err != nil {
		return err
	}
	e.SetCookie(&http.Cookie{
		Name:  cookieNameToken,
		Value: signedToken,
		Path:  "/",
	})
	log.Debugf(ctx, "signed token: %v", signedToken)

	http.Redirect(e.Response(), e.Request(), "/client/", 302)
	return nil
}
```

* 秘密鍵の取得などの認可関係の処理は `server/app/auth.go` にまとめた

```go:server/app/auth.go
func GetHMACKey() (uuid.UUID, error) {
	key := os.Getenv("HMAC_KEY")
	if key == "" {
		return uuid.NewV4(), nil
	}
	return uuid.FromString(key)
}
```

* 秘密鍵が環境変数にセットされていない場合は毎回新規に生成するので、一度設定せずに発行し、ログから生成された秘密鍵をコピーして環境変数に設定すると楽。

```yaml:server/gae/secret.yaml
env_variables:
  GOOGLE_CLIENT_ID: "xxxxx"
  GOOGLE_CLIENT_SECRET: "xxxxx"
  HMAC_KEY: "00000000-0000-0000-0000-000000000000"
```


# リクエスト受信時にアクセストークンをチェックする

## アクセストークンをチェックするエンドポイントを定義する

* リクエスト時にミドルウェアを使ってアクセストークンをチェックするエンドポイント `/api/hello` を作成する。
  * ミドルウェアとはハンドラをラップする形で呼び出される関数で、リクエスト共通で利用される前処理や後処理を定義して実行することができる。
  * `echo.Group("PATH" [, ...MIDDLEWARES])` を使うと特定のパス以下のハンドラにまとめてミドルウェアを設定することができる。
* 動作確認のためチェックしないエンドポイント `/hello` も用意する。

```go:server/app/handler.go
func NewHandler() http.Handler {
	
	// ... 略 ...

	api := e.Group("/api", AuthorizationMiddleware)
	api.GET("/hello", func(e echo.Context) error {
		return e.JSON(http.StatusOK, struct{ Message string }{"hello authorized"})
	})

	e.GET("/hello", func(e echo.Context) error {
		return e.JSON(http.StatusOK, struct{ Message string }{"hello not authorized"})
	})
```

## アクセストークンをチェックするミドルウェアを用意する

* アクセストークンはリクエストの "Authorization" ヘッダーで "Bearer ACCESS_TOKEN" という値で送られてくることを想定する。
* ミドルウェアでは以下の処理を行う。
  1. ヘッダーからアクセストークンの取得
  1. アクセストークンの署名と有効期限の検証
  1. アクセストークンからユーザーIDの取得

```go:server/app/auth.go
func AuthorizationMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
	return func(e echo.Context) error {
		ctx := appengine.NewContext(e.Request())
		token, err := GetTokenFromRequest(e.Request())
		if err != nil {
			log.Errorf(ctx, "Get token: %v", err)
			return echo.NewHTTPError(http.StatusUnauthorized)
		}
		if claims, ok := token.Claims.(jwt.MapClaims); !ok || !token.Valid {
			log.Errorf(ctx, "Get Claims: %v", err)
			return echo.NewHTTPError(http.StatusUnauthorized)
		} else if sub, ok := claims["sub"].(string); !ok {
			log.Errorf(ctx, "Get Sub: %v", err)
			return echo.NewHTTPError(http.StatusUnauthorized)
		} else {
			e.Set("UserID", sub)
		}
		return next(e)
	}
}

func GetTokenFromRequest(r *http.Request) (*jwt.Token, error) {
	h := r.Header.Get("Authorization")

	if h == "" {
		return nil, errors.New("Auth header empty")
	}

	parts := strings.SplitN(h, " ", 2)
	if !(len(parts) == 2 && parts[0] == "Bearer") {
		return nil, errors.New("Invalid auth header")
	}

	return jwt.Parse(parts[1], func(token *jwt.Token) (interface{}, error) {
		id, err := GetHMACKey()
		if err != nil {
			return nil, err
		}
		return id.Bytes(), nil
	})
}
```

# アクセストークンを使って Angular から GAE にリクエストを送信する

* Angular のコンポーネントで Cookie からアクセストークンを取得し、リクエストヘッダーに付与してリクエストを送信する関数を実装する。

```ts:client/src/app/app.component.ts
export class AppComponent {
  token: string;

  constructor(private http: HttpClient, private cookie: CookieService) {
    this.token = this.cookie.get('TOKEN');
  }

  onClickAuthrized() {
    this.http.get('/api/hello', {
      headers: new HttpHeaders({ 'Authorization': 'Bearer ' + this.token })
    }).subscribe(r => console.log(r));
  }

  onClickNotAuthrized() {
    this.http.get('/hello', {
      headers: new HttpHeaders({ 'Authorization': 'Bearer ' + this.token })
    }).subscribe(r => console.log(r));
  }
```

* クリックで実装した関数を実行するボタンを追加する。

```html:client/src/app/app.component.html
<button (click)="onClickAuthrized()">AUTHORIZED</button>
<button (click)="onClickNotAuthrized()">NOT AUTHORIZED</button>
```

* `onClickAuthrized` と `onClickNotAuthrized` の Authorization ヘッダーの定義の行をコメントにすることでアクセストークンを付与しない場合の確認ができる。

# おわりに

* これでユーザーの認証認可ができるようになったので、次回はデータの CRUD を実装する。