なんちゃってOAuth2/OpenID Connectサーバを自作する 

今回は、OAuth2サーバ、正確に言うとOpenID Connectサーバを自作します。以降、OpenID Connectサーバと呼びます。

それはなぜか。。。IDトークンやアクセストークンを使ったサイトが増えているけれども、それと連携を試そうとしたときに、わざわざAWS CognitoにリダイレクトURLやらクライアントIDを設定するのが面倒だから。それから、実際にどのような連携をしているかをじかに見たいから。です。

で、サーバを立ち上げはしますが、ユーザIDやパスワードのチェックも行わないですし、クライアントIDやリダイレクトURLのチェックも行わないので、なんちゃってサーバです。ですが、JSON Web Tokenの形にはしているので、お手軽自動トークン生成機を作っているような感じです。

なんちゃってサーバなので、他サイトの連携が確認できたら、正規のOpenID Connectサーバに切り替えましょう。くれぐれも正式運用で使わないでください。誰でも彼でもトークンを生成しちゃいますので。

ソース一式を上げておきました。
　https://github.com/poruruba/openid_server

なんだかんだ言って、HTTPではなくHTTPSが必要なケースがほとんどであるため、SSL証明書の取得には以下も参考にしてください。
　[SSL証明書を取得しよう](https://qiita.com/poruruba/items/59d6197166bb09049f53)

(2018/11/10 修正)
フラグメント識別子で認可コードを返していなかったり、Client Credentials Grantに対応していなかったりと、中途半端だったので治しました。ついでに、redirect.htmlを追加して、使いやすくしました。

(2019/6/28 修正)
トークン取得エンドポイントが間違っていました。

(2020/1/4)
「 [試しに「OpenID Connect Provider Certification」を通してみた](https://qiita.com/poruruba/items/b930acf167bae6d3fe3c) 」 で検証したところ、トークン取得の方法には複数あるようで、追加実装しました。

# Swagger定義ファイルの作成

OpenID Connectサーバのエンドポイントを定義します。
OpenID Connectの仕様にしたがって、以下の4つ＋1つを定義します。

* /oauth2/authorize
* /oauth2/token
* /.well-known/jwks.json
* /.well-known/openid-configuration

* /oauth2/userInfo （必要に応じて）

(参考)
http://openid-foundation-japan.github.io/openid-connect-core-1_0.ja.html

以下のような感じです。

```swagger.yaml
swagger: '2.0'
info:
  version: 'first version'
  title: OpenID Connect Server
host: localhost:10080
basePath: /

schemes:
  - http
  - https

consumes:
  - application/json
produces:
  - application/json

securityDefinitions:
  basicAuth:
    type: basic
  tokenAuth:
    type: apiKey
    name: Authorization
    in: header
  apikeyAuth:
    type: apiKey
    in: header
    name: X-API-KEY
#  jwtAuth:
#    authorizationUrl: ""
#    flow: "implicit"
#    type: "oauth2"
#    x-google-issuer: "https://cognito-idp.ap-northeast-1.amazonaws.com/【CognitoのプールID】"
#    x-google-jwks_uri: "https://cognito-idp.ap-northeast-1.amazonaws.com/【CognitoのプールID】/.well-known/jwks.json"
#    x-google-audiences: "【CognitoのアプリクライアントID】"

paths:
  /swagger:
    x-swagger-pipe: swagger_raw

  /.well-known/openid-configuration:
    get:
      x-swagger-router-controller: routing
      operationId: oauth2_openid_config
      responses:
        200:
          description: Success
          schema:
            type: object

  /.well-known/jwks.json:
    get:
      x-swagger-router-controller: routing
      operationId: oauth2_jwks_json
      responses:
        200:
          description: Success
          schema:
            type: object

  /oauth2/authorize-process:
    get:
      x-swagger-router-controller: routing
      operationId: oauth2_authorize_process
      parameters:
        - in: query
          name: client_id
          required: true
          type: string
        - in: query
          name: userid
          required: true
          type: string
        - in: query
          name: password
          required: true
          type: string
        - in: query
          name: redirect_uri
          required: true
          type: string
      responses:
        200:
          description: Success
          schema:
            type: object

  /oauth2/authorize:
    get:
      x-swagger-router-controller: routing
      operationId: oauth2_authorize
      parameters:
        - in: query
          name: response_type
          required: true
          type: string
        - in: query
          name: client_id
          required: true
          type: string
        - in: query
          name: redirect_uri
          required: true
          type: string
        - in: query
          name: scope
          type: string
        - in: query
          name: state
          type: string
      responses:
        200:
          description: Success
          schema:
            type: object

  /oauth2/token:
    post:
      x-swagger-router-controller: routing
      operationId: oauth2_token
      security:
        - basicAuth: []
      consumes:
        - application/x-www-form-urlencoded
      parameters:
        - in: formData
          name: grant_type
          type: string
          required: true
        - in: formData
          name: client_id
          type: string
        - in: formData
          name: code
          type: string
        - in: formData
          name: scope
          type: string
        - in: formData
          name: redirect_uri
          type: string
        - in: formData
          name: refresh_token
          type: string
      responses:
        200:
          description: Success
          schema:
            type: object

  /oauth2/userInfo:
    get:
      x-swagger-router-controller: routing
      operationId: oauth2_user_info
      security:
        - tokenAuth: []
      responses:
        200:
          description: Success
          schema:
            type: object
    post:
      x-swagger-router-controller: routing
      operationId: oauth2_user_info
      consumes:
        - application/x-www-form-urlencoded
      parameters:
        - in: formData
          name: access_token
          type: string
      responses:
        200:
          description: Success
          schema:
            type: object

definitions:
  Empty:
    type: "object"
    title: "Empty Schema"
```

# サーバの実装

手っ取り早く、ソースコードを載せちゃいます。
Lambdaで動くようにしています。
具体的な内容は、後程の(解説)という章で説明します。

```index.js
'use strict';

const base_url = process.env.BASE_URL || 'http://localhost:10080';
const page_url = process.env.PAGE_URL || 'http://localhost:10080';
const issuer = process.env.ISSUER || 'http://localhost:10080';

const login_url = process.env.LOGIN_URL || (page_url + '/login/login.html');
const keyid = process.env.KEYID || 'testkeyid';
const expire = 60 * 60;

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
var Response = require(HELPER_BASE + 'response');
var Redirect = require(HELPER_BASE + 'redirect');

var fs = require('fs');
var tojwks = require('rsa-pem-to-jwk');
var jwt = require('jsonwebtoken');
const { URL, URLSearchParams } = require('url');

var jwkjson = null;
var priv_pem = fs.readFileSync('./api/controllers/oauth2/jwks/privkey.pem');

function make_access_token(client_id, scope){
    var payload_access_token = {
        token_use: 'access',
        scope: scope,
        client_id: client_id,
    };
    var access_token = jwt.sign(payload_access_token, priv_pem, {
        algorithm: 'RS256',
        expiresIn: expire,
        issuer: issuer,
        subject: client_id,
        keyid: keyid
    });

    var tokens = {
        "access_token" : access_token,
        "token_type" : "Bearer",
        "expires_in" : expire
    };
    return tokens;
}

function make_tokens(client_id, userid, scope, refresh = true){
    var payload_id = {
        token_use: 'id',
        "cognito:username": userid,
        email: userid + '@test.com',
	};
    var id_token = jwt.sign(payload_id, priv_pem, {
        algorithm: 'RS256',
        expiresIn: expire,
        audience: client_id,
        issuer: issuer,
        subject: userid,
        keyid: keyid,
    });
    var payload_access_token = {
        token_use: 'access',
        scope: scope,
        "cognito:username": userid,
        email: userid + '@test.com',
    };
    var access_token = jwt.sign(payload_access_token, priv_pem, {
        algorithm: 'RS256',
        expiresIn: expire,
        audience: client_id,
        issuer: issuer,
        subject: userid,
        keyid: keyid,
    });

    var tokens = {
        access_token : access_token,
        id_token : id_token,
        token_type : "Bearer",
        expires_in : expire
    };
    if( refresh ){
        var refresh_token = Buffer.from(client_id + ':' + userid + ':' + scope, 'ascii').toString('hex');
        tokens.refresh_token = refresh_token;
    }

    return tokens;
}

exports.handler = (event, context, callback) => {
    if( event.path == '/oauth2/token'){
        // Lambda＋API Gatewayの場合はこちら
        // var params = new URLSearchParams(event.body);
        // swagger_nodeの場合はこちら
        var params = Object.entries(JSON.parse(event.body)).reduce((l,[k,v])=>l.set(k,v), new Map());

        var grant_type = params.get('grant_type');
        if( grant_type == 'authorization_code' || grant_type == "refresh_token"){
            var code;
            if( grant_type == "refresh_token" )
                code = Buffer.from(params.get('refresh_token'), 'hex').toString('ascii');
            else
                code = Buffer.from(params.get('code'), 'hex').toString('ascii');
            var code_list = code.split(':');
            
            var client_id = code_list[0];
            var userid = code_list[1];
            var scope = code_list[2];

            var tokens = make_tokens(client_id, userid, scope, grant_type != "refresh_token" );

            callback(null, new Response(tokens));
        }else if( grant_type == 'client_credentials'){
            var scope = params.get('scope');
            var client_id = params.get('client_id');

            var tokens = make_access_token(client_id, scope);

            callback(null, new Response(tokens));
        }
    }else if( event.path == '/oauth2/authorize-process' ){
        var { client_id, userid, password, redirect_uri, response_type, scope, state } = event.queryStringParameters;

        if( response_type == 'token'){
            var tokens = make_tokens(client_id, userid, scope);

            var url = redirect_uri + '#id_token=' + tokens.id_token + '&access_token=' + tokens.access_token + '&refresh_token=' + tokens.refresh_token 
                                        + '&token_type=' + tokens.token_type + '&expires_in=' + tokens.expires_in;
            if( state )
                url += '&state=' + decodeURIComponent(state);

            callback(null, new Redirect(url));
        }else if( response_type == 'code' ){
            var code = Buffer.from(client_id + ':' + userid + ':' + scope, 'ascii').toString('hex');

            var url = redirect_uri + '?code=' + code;
            if( state )
                url += '&state=' + decodeURIComponent(state);

            callback(null, new Redirect(url));
        }
    }else if( event.path == '/oauth2/authorize' ){
        var { client_id, redirect_uri, response_type, scope, state } = event.queryStringParameters;

        var url = login_url + '?client_id=' + client_id + '&redirect_uri=' + encodeURIComponent(redirect_uri) + '&response_type=' + response_type;
        if( scope )
            url += '&scope=' + encodeURIComponent(scope);
        if( state )
            url += '&state=' + encodeURIComponent(state);

        callback(null, new Redirect(url));
    }else if( event.path == '/oauth2/userInfo'){
        if( event.httpMethod == 'GET'){
//            var token = jwt_decode(event.headers.authorization);
            var token = event.requestContext.authorizer.claims;

            callback(null, new Response(token));
        }else if( event.httpMethod == 'POST' ){
            // Lambda＋API Gatewayの場合はこちら
            // var params = new URLSearchParams(event.body);
            // swagger_nodeの場合はこちら
            var params = Object.entries(JSON.parse(event.body)).reduce((l,[k,v])=>l.set(k,v), new Map());
            var access_token = params.get('access_token');
            if( !access_token )
                access_token = event.headers.authorization;

            callback(null, new Response(jwt_decode(access_token)));
        }
    }else if( event.path == '/.well-known/jwks.json'){
        if( jwkjson == null ){
            jwkjson = {
                keys: [
                    tojwks(priv_pem, {use: 'sig', kid: keyid, alg: 'RS256'}, 'pub')
                ]
            };
        }

        callback(null, new Response(jwkjson));
    }else if( event.path == '/.well-known/openid-configuration' ){
        var configjson = {
            authorization_endpoint: base_url + "/oauth2/authorize",
            id_token_signing_alg_values_supported: [
                "RS256"
            ],
            issuer: issuer,
            jwks_uri: base_url + "/.well-known/jwks.json",
            response_types_supported: [
                "code",
                "token"
            ],
            scopes_supported: [
                "openid",
                "profile"
            ],
            subject_types_supported: [
                "public"
            ],
            token_endpoint: base_url + "/oauth2/token",
            token_endpoint_auth_methods_supported: [
                "client_secret_basic"
            ],
            userinfo_endpoint: base_url + "/oauth2/userInfo"
        };

        callback(null, new Response(configjson));
    }
};
```

見ての通り、利用するnpmモジュールは以下の通りです。

* jsonwebtoken
* rsa-pem-to-jwk

# オレオレ証明書の作成

今回は、トークンの署名形式として「RS256」を採用します。
「RSASSA-PKCS1-v1_5 using SHA-256」を示しているのですが、要は署名にRSA公開鍵ペアを使うということです。
ですので、まずは、opensslを使って公開鍵ペアを作りましょう。

> openssl genrsa -out privkey.pem 2048

これで、PEM形式のprivkey.pemというファイルが出来上がっているかと思います。(★改行はLFにしないといけないようです）
これを適当なフォルダに置いて、index.jsの8行目のあたりにそのパスを指定します。

> var priv_pem = fs.readFileSync('./api/controllers/oauth2/jwks/privkey.pem');

# ユーザID/パスワード入力ページの作成

よくGoogleアカウント連携するサイトにアクセスすると、Googleのページに飛んでGmailとパスワードを入力したりしますよね。
それと同じように、ユーザID・パスワードを入力する画面を作っておきます。
今回は特に何も処理しないので、盲目的に以下のHTMLページ(サインインページと呼びます)を用意しておいてください。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>サインイン</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>

  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>サインイン</h1>

        <form method="get" v-bind:action="authorize_process">
            <label>ユーザID</label>
            <input type="text" name="userid" class="form-control">
            <label>パスワード</label>
            <input type="password" name="password" class="form-control">

            <input type="hidden" name="client_id" v-model="client_id">
            <input type="hidden" name="redirect_uri" v-model="redirect_uri">
            <input type="hidden" name="response_type" v-model="response_type">
            <input type="hidden" name="scope" v-model="scope">
            <input type="hidden" name="state" v-model="state">
            <br>
            <button type="submit" class="btn btn-default">サインイン</button> 
        </form>
    </div>
    
    <script src="js/start_login.js"></script>
</body>
```

```start_login.js
cconst base_url = 'http://localhost:10080';

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',
        client_id: '',
        redirect_uri: '',
        response_type: '',
        scope: '',
        state: '',
        authorize_process: base_url + '/oauth2/authorize-process'
    },
    computed: {
    },
    methods: {
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
        this.client_id = searchs.client_id;
        this.redirect_uri = searchs.redirect_uri;
        this.response_type = searchs.response_type;
        this.scope = searchs.scope;
        this.state = searchs.state;
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );
```

このページを、/login/login.html にアクセスしたときに表示されるようにしてください。

ちなみに、action="/oauth2/authorize-process" とある通り、ここで入力されたユーザIDとパスワードが、swagger定義ファイルにあったエンドポイントに届くようにしています。

# (解説) /oauth2/authorize

まずは、認証エンドポイントから。
何をしているかというと、さきほど作成したサインインページにリダイレクトしているだけです。
認証エンドポイントで指定されたパラメータは後で使うのでそれも引き継いでいます。

(参考)
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/authorization-endpoint.html

# (解説) /oauth2/authorize-process

これがサインインページからの遷移先です。
パラメータresponse_typeの値によって処理が異なります。
tokenだった場合は、即トークンを生成し返しています。
codeだった場合は、認可コードを生成し返すのですが、実装の手抜きのために、クライアントIDとユーザIDとスコープを16進数文字列にしたものを認可コードとして、認証エンドポイントで指定されたリダイレクトURLに戻しています。
本来であれば、ここで、ユーザID・パスワードの検証を行うべきですがやってません！

# (解説) /oauth2/token

トークンエンドポイントです。
パラメータgrant_typeで指定された値によって処理が異なりますが、やっていることはほぼ同じです。
・authorization_code
・refresh_token
・client_credentials
前者の2つは、引数で与えられた認可コードまたはリフレッシュトークンに、クライアントIDとユーザIDとスコープを16進数文字列にしておいたので、それをもとにトークンを生成し返しています。
本来はここでAuthrizasionヘッダーにあるクライアントシークレットの確認をすべきですがやってません！

(参考)
https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html

# (解説) トークン生成

トークン生成は、関数 make_tokens の部分です。
入力として、クライアントIDとユーザIDとスコープを指定します。
これらが、サインインしてきた人によって変わるためです。
それ以外の項目は、変わらないので環境変数で指定できるようにしています。

* LOGIN_URL：ログインページを配置した場所のURLです。
* BASE_URL：立ち上げたRESTfull環境のエンドポイントのベースURLを指定します。
* PAGE_URL：立ち上げたRESTfull環境の静的ページのベースURLを指定します。
* KEYID：公開鍵の識別子です。なんでもよいです。トークンにこの値がkidとして埋め込まれます。
* ISSUER：トークンの発行者名です。トークンにこの値がissとして埋め込まれます。

トークン生成には、「jsonwebtoken」を使わせていただきました。

あと、リフレッシュトークンを作っているのですが、/oauth2/token のところでリフレッシュトークンを扱いやすい(手抜き)ように、クライアントIDとユーザIDとスコープを16進数文字列にしておきます。

以下が、生成されたトークンの例です。

```
{
access_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRoaXNJc0tleUlkIn0.eyJ0b2tlbl91c2UiOiJhY2Nlc3MiLCJzY29wZSI6InRlc3Rfc2NvcGUiLCJpYXQiOjE1NDE2ODIyODMsImV4cCI6MTU0MTY4NTg4MywiYXVkIjoidGVzdF9jbGllbnRpZCIsImlzcyI6IlRoaXNJc0lzc3VlciIsInN1YiI6InRlc3RfdXNyaWQifQ.Qls8rE3WagwKZYqqbAG9FFlmp0cbRfK-S76xc9Xr0JY3QAhZQLEHUrlUwXA-xdTIwXrEjw5KJgHa96z9RHVeTRrsgHlSk0iUiL3ZhgPcOc6kPr2MEA8CQ4b-PpxpRepEvfsHAspSs4nFbG5HGJ2uScBreaJhoIo8aXvdpsY0MQtnFRdg9fJhEx6ZNMab05e3Gx61MaeD_q5u6CYVnTztP5l6xQnHsLiWYCglDxYVkvRmCtfpH7X8Jbv1hbdohlhhnZ5QJIEck3F4mUu0icbtLimbFBhT7BHASO4Ve9wAhkot2NpJPLZmzbKnfQ-qpbd-9Do0gt1P4CymZlJBzaRz6w",
refresh_token: "dGVzdF9jbGllbnRpZDp0ZXN0X3VzcmlkOnRlc3Rfc2NvcGU=",
id_token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IlRoaXNJc0tleUlkIn0.eyJ0b2tlbl91c2UiOiJpZCIsImlhdCI6MTU0MTY4MjI4MywiZXhwIjoxNTQxNjg1ODgzLCJhdWQiOiJ0ZXN0X2NsaWVudGlkIiwiaXNzIjoiVGhpc0lzSXNzdWVyIiwic3ViIjoidGVzdF91c3JpZCJ9.p9OADDHeLSObLO4vsyw2_muECubTxcL96gsxiUfQI-jYTIGQnYp7r_qxn0Y-CeitP826Mz4vYhFoauGhTHSoPfPbzuXq0Ceo3Qs2_qEofpG_0PNXL5u0AjJlmgJLqwXpylZNgTPvQ1ETqVuX2kbh5l3TWfQP9kapmkJM1RX4c2GQvVKPPMkljRoqhB97nyp6Tf6gwM3Dz1m9gqybDe3qKcXNdBY7vVJpk3YSV_Uk78WuTVyeu7s_8Snsxka5-GBTSPq0opSOxPEu4vh1bXnuUOdevlxgcNwbF9hRt__NyE0aR0k5RyuUDz7VbRIm5Nv5OnsHRyM9VFICXAseFrSndQ",
token_type: "Bearer",
expires_in: 3600
}
```

これを https://jwt.io でデコードしてみると、以下となっているのがわかります。

以下、IDトークン

```
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "【環境変数KEYIDで指定されたもの】"
}
{
  "token_use": "id",
  "iat": 1541682283,
  "exp": 1541685883,
  "aud": "test_clientid",
  "iss": "【環境変数ISSUERで指定されたもの】",
  "sub": "test_usrid"
}
```

以下、アクセストークン

```
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "【環境変数KEYIDで指定されたもの】"
}
{
  "token_use": "access",
  "scope": "test_scope",
  "iat": 1541682283,
  "exp": 1541685883,
  "aud": "test_clientid",
  "iss": "【環境変数ISSUERで指定されたもの】",
  "sub": "test_usrid"
}
```

# (解説) jwks.json

これは、トークンを取得した人が、トークンに付与された署名を検証するための公開鍵です。
opensslで公開鍵ペアを作りましたが、このうちの公開鍵をJSON形式にしたものです。このフォーマットは、OpenID Connect仕様で決まっているそうです。
以下のような出力が得られます。

```
{
 keys: [
 {
    kty: "RSA",
    use: "sig;",
    kid: "【環境変数KEYIDで指定されたもの】",
    alg: "RS256",
    n: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    e: "AQAB"
  }
]
}
```

# (解説) openid-configuration

これは、サーバの情報を表明するためのものです。
このフォーマットは、OpenID Connect仕様で決まっているそうです。
以下のような出力が得られます。

```
{
 authorization_endpoint: "【環境変数BASE_URLで指定されたもの】/oauth2/authorize",
 id_token_signing_alg_values_supported: [
 "RS256"
],
 issuer: "【環境変数ISSUERで指定されたもの】",
 jwks_uri: "【環境変数BASE_URLで指定されたもの】/oauth2/.well-known/jwks.json",
 response_types_supported: [
  "code",
  "token"
 ],
 scopes_supported: [
  "openid",
  "profile"
 ],
 subject_types_supported: [
  "public"
 ],
 token_endpoint: "【環境変数BASE_URLで指定されたもの】/oauth2/token",
 token_endpoint_auth_methods_supported: [
  "client_secret_basic"
 ]
}
```

# 動作確認のためのトップページ

このページは必須ではないのですが、参考までに、このサーバサイトを利用してトークン生成するための、簡単なトップページを示しておきます。

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>サインイン開始ページ</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>
  
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>サインイン開始ページ</h1>

        <label>authorize_endpoint</label>
        <input type="text" name="authorize_endpoint" class="form-control" v-model="authorize_endpoint">
        <br>

        <form method="get" v-bind:action="authorize_endpoint">
            <label>response_type</label>
            <select name="response_type" class="form-control">
                <option value="token">token</option>
                <option value="code">code</option>
            </select>
            <label>client_id</label>
            <input type="text" name="client_id" class="form-control">
            <label>redirect_uri</label>
            <input type="text" name="redirect_uri" class="form-control" v-model="redirect_uri">
            <label>scope</label>
            <input type="text" name="scope" class="form-control">
            <label>state</label>
            <input type="text" name="state" class="form-control">
            <br>
            <button type="submit" class="btn btn-default">サインイン開始</button> 
        </form>
        <br>
        <a v-bind:href="redirect_uri">トークン生成ページへ</a>
    </div>
    <script src="js/start.js"></script>
</body>
```

```start.js
const base_url = 'http://localhost:10080';
const page_url = 'http://localhost:10080';

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        redirect_uri: page_url + '/login/redirect.html',
        authorize_endpoint: base_url + '/oauth2/authorize',
        authorize_direct_endpoint: base_url + '/oauth2/authorize-direct'
    },
    computed: {
    },
    methods: {
    },
    created: function(){
    },
    mounted: function(){
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );
```

それから、リダイレクト先に表示するページです。

```redirect.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>リダイレクトページ</title>

  <script src="js/methods_utils.js"></script>
  <script src="js/vue_utils.js"></script>
    
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>リダイレクトページ</h1>

        <div v-if="access_token || id_token">
            <div v-if="id_token">
                <label>id_token</label>
                <p>{{id_token}}</p>
            </div>
            <div v-if="access_token">
                <label>access_token</label>
                <p>{{access_token}}</p>
            </div>
            <div v-if="refresh_token">
                <label>refresh_token</label>
                <p>{{refresh_token}}</p>
            </div>
            <label>expires_in</label>
            <p>{{expires_in}}</p>
            <div v-if="token_state">
                <label>state</label>
                <p>{{token_state}}</p>
            </div>
        </div>
        <div v-else>
            <label>token_endpoint</label>
            <input type="text" name="token_endpoint" class="form-control" v-model="token_endpoint">
            <br>

            <label>grant_type</label>
            <select name="grant_type" class="form-control" v-model="grant_type">
                <option value="authorization_code">authorization_code</option>
                <option value="refresh_token">refresh_token</option>
                <option value="client_credentials">client_credentials</option>
            </select>
            <label>client_id</label>
            <input type="text" name="client_id" class="form-control" v-model='client_id'>
            <label>client_secret</label>
            <input type="text" name="client_secret" class="form-control" v-model='client_secret'>
            <div v-if="grant_type=='authorization_code'">
                <label>redirect_uri</label>
                <input type="text" name="redirect_uri" class="form-control" v-model='redirect_uri'>
            </div>
            <div v-if="grant_type=='client_credentials'">
                <label>scope</label>
                <input type="text" name="scope" class="form-control" v-model='scope'>
            </div>
            <div v-if="grant_type=='authorization_code'">
                <label>code</label>
                <input type="text" name="code" class="form-control" v-model='code'>
            </div>
            <div v-if="grant_type=='refresh_token'">
                <label>refresh_token</label>
                <input type="text" name="refresh_token" class="form-control" v-model='refresh_token'>
            </div>
            <div v-if="state">
                <label>state</label>
                <p>{{state}}</p>
            </div>
            <br>
            <button v-on:click="token_call()" class="btn btn-default">トークン生成</button>
        </div>
        <br>
        <button class="btn btn-default" v-on:click="get_userinfo()">userInfo</button><br>
        <div v-if="userinfo">
            <label>usesrinfo</label>
            <p>{{userinfo}}</p>
        </div>
        <a class="btn btn-default" v-bind:href="logout_endpoint">ログアウト</a><br>
        <a v-bind:href="top_url">サインイン開始ページに戻る</a>
    </div>
    <script src="js/start_redirect.js"></script>
</body>
```

```start_redirect.js
var base_url = 'http://localhost:10080';
var page_url = 'http://localhost:10080';

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',
        token_endpoint: base_url + '/oauth2/token',
        grant_type: 'authorization_code',
        client_id: '',
        client_secret: '',
        code: '',
        refresh_token: '',
        redirect_uri: page_url + '/login/redirect.html',
        scope: '',
        state: null,
        id_token: null,
        access_token: null,
        refresh_token: null,
        expires_in: 0,
        token_state: null,
        top_url: './index.html',
        usesrinfo_endpoint: base_url + '/oauth2/userInfo',
        userinfo: null
    },
    computed: {
        logout_endpoint: function(){
            return base_url + '/logout?client_id=' + this.client_id + '&logout_uri=' + this.redirect_uri;
        }
    },
    methods: {
        token_call: function(){
            var code_or_refresh = (this.grant_type == 'authorization_code') ? this.code : (this.grant_type == 'refresh_token') ? this.refresh_token : null;
            var params = {
                grant_type: this.grant_type,
                client_id: this.client_id,
                client_secret: this.client_secret,
                redirect_uri: this.redirect_uri
            };
            if( this.grant_type == 'authorization_code')
                params.code = code_or_refresh;
            else if( this.grant_type == 'refresh_token')
                params.refresh_token = code_or_refresh;

            do_post_basic(this.token_endpoint, params, this.client_id, this.client_secret )
            .then(result =>{
                console.log(result);
                this.id_token = result.id_token;
                this.access_token = result.access_token;
                this.refresh_token = result.refresh_token;
                this.expires_in = result.expires_in;
            });
        },
        get_userinfo: function(){
            do_get_token(this.usesrinfo_endpoint, this.access_token)
            .then( result =>{
                this.userinfo = result;
            });
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
        
        this.code = searchs.code;
        this.state = searchs.state;
        this.id_token = hashs.id_token;
        this.access_token = hashs.access_token;
        this.refresh_token = hashs.refresh_token;
        this.expires_in = hashs.expires_in;
        this.token_state = hashs.state;
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function do_post_urlencoded(url, grant_type, client_id, client_secret, redirect_uri, code_or_refresh){
    var params = {
        grant_type: grant_type,
        client_id: client_id,
        client_secret: client_secret,
        redirect_uri: redirect_uri
    };
    if( grant_type == 'authorization_code')
        params.code = code_or_refresh;
    else if( grant_type == 'refresh_token')
        params.refresh_token = code_or_refresh;

    var data = new URLSearchParams();
    for( var name in params )
        data.append(name, params[name]);

    var basic = 'Basic ' + btoa(client_id + ':' + client_secret);
    const headers = new Headers( { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : basic } );
    
    return fetch(url, {
        method : 'POST',
        body : data,
        headers: headers
    })
    .then((response) => {
        if( response.status != 200 )
            throw 'status is not 200';
        return response.json();
    })
}

function do_post_basic(url, params, client_id, client_secret){
    var data = new URLSearchParams();
    for( var name in params )
        data.append(name, params[name]);

    var basic = 'Basic ' + btoa(client_id + ':' + client_secret);
    const headers = new Headers( { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : basic } );
    
    return fetch(url, {
        method : 'POST',
        body : data,
        headers: headers
    })
    .then((response) => {
        if( response.status != 200 )
            throw 'status is not 200';
        return response.json();
    })
}

function do_get_token(url, token){
    const headers = new Headers( { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : 'Bearer' + token } );
    
    return fetch(url, {
        method : 'GET',
        headers: headers
    })
    .then((response) => {
        return response.json();
    })
}
```

以上です。