19
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

なんちゃって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証明書を取得しよう

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

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

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

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();
    })
}

以上です。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
19
Help us understand the problem. What are the problem?