今回は、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: '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で動くようにしています。
具体的な内容は、後程の(解説)という章で説明します。
'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ページ(サインインページと呼びます)を用意しておいてください。
<!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>
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"
]
}
動作確認のためのトップページ
このページは必須ではないのですが、参考までに、このサーバサイトを利用してトークン生成するための、簡単なトップページを示しておきます。
<!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>
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 );
それから、リダイレクト先に表示するページです。
<!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>
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();
})
}
以上です。