Skip to content

Commit

Permalink
とりあえず実装。未テスト
Browse files Browse the repository at this point in the history
  • Loading branch information
hhorikawa committed Jul 7, 2024
1 parent 818f9ad commit e8bbd20
Show file tree
Hide file tree
Showing 22 changed files with 368 additions and 209 deletions.
268 changes: 165 additions & 103 deletions app/controllers/authorizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,118 @@
# RFC 6749 (Oct 2012) The OAuth 2.0 Authorization Framework
# https://www.rfc-editor.org/rfc/rfc6749

=begin
Sorcery: 明示的にログインした時刻
Activity Logging モジュールを使う.
- `last_login_at`
- `last_activity_at` などのフィールドをデータベースに保存.
Sorcery は, `Config.after_login` と `Config.after_remember_me` 設定がある。
前者は `login(*credentials)` 内から呼び出される。
Activity Logging の `last_login_at` は明示的にログインした時刻として使ってよい
=end


# 認可エンドポイント
class AuthorizationsController < ApplicationController
rescue_from Rack::OAuth2::Server::Authorize::BadRequest do |e|
@error = e
logger.info e.backtrace[0,10].join("\n")
render :error, status: e.status
render :error, layout:false, status: e.status
end


# GET
# 認証の開始: 確認画面を表示
# authorization_endpoint: "http://localhost:4000/authorizations/new"
def new
# viewstate に必要:
# @client, @response_type, @redirect_uri, @scopes,
# @_request_, @request_uri,
# @request_object
call_authorization_endpoint false
# まず, request object をつくる. 内部で client 検査
@request_object = RequestObject.find_or_build_from_params params

# 妥当性を確認する
call_authorization_endpoint(@request_object) do |req, res|
request_validation(req, res)
end
return if performed? # エラー / リダイレクトの場合

# ログインしていなければ、または max_age を経過していたら, ログインを求め
# る.
# 本来は, 実際にログインするユーザ.
if logged_in? && (max_age = @request_object.max_age) &&
current_user.last_login_at < max_age.seconds.ago
flash[:alert] = 'Exceeded Max Age, Login Again'
logout() # ここでセッションがリセットされる
end
# かならず `logout()` より後ろに書く
require_login()
return if performed? # 未ログイン: リダイレクトの場合

# ここにはかならずログイン済みの状態
=begin
過去に認可を得ている scope であれば, そのまま RP にリダイレクトバックしてよい
-> これは, アクセストークンの期限より長い。
TODO: (FakeUser, Client, Scope) 表が必要
=end

@viewstate = SecureRandom.alphanumeric(12)
session[@viewstate] = {
params: params # 再現できるので、オリジナルのパラメータだけ.
}
end


# POST
# ユーザの approve/deny を受けて、RPにリダイレクトバックする.
def create
call_authorization_endpoint true, params[:approve]
req = session[params['_viewstate']][:params]
@request_object = RequestObject.find_or_build_from_params req

@fake_user = FakeUser.find params[:fake_user]
@authorized_scopes = params[:scope] # ● 配列になるか?
raise "check"
approved = params[:approve]

call_authorization_endpoint(@request_object) do |req, res|
# ユーザによる approve/deny
if approved
consent_and_redirect_back req, res
else
req.access_denied!
end
end
end


private

def call_authorization_endpoint allow_approval, approved = false
endpoint = authorization_endpoint_authenticator allow_approval, approved
# [ 200, {"Content-Type" => "text/plain"}, ["Hello Rack!\n\n"] ]
status, header, res_body = endpoint.call(request.env)
# だいぶ頭の痛い造りになっている
# コールバックを強制させるつくりなのも厳しい。
@@authorize_handlers = {
# rack-oauth2
'code' => Rack::OAuth2::Server::Authorize::Code ,
'token' => Rack::OAuth2::Server::Authorize::Token,
'code token' => Rack::OAuth2::Server::Authorize::Extension::CodeAndToken ,
# openid_connect
'id_token' => Rack::OAuth2::Server::Authorize::Extension::IdToken ,
'id_token token' => Rack::OAuth2::Server::Authorize::Extension::IdTokenAndToken ,
'code id_token' => Rack::OAuth2::Server::Authorize::Extension::CodeAndIdToken ,
'code id_token token' => Rack::OAuth2::Server::Authorize::Extension::CodeAndIdTokenAndToken ,
}

def call_authorization_endpoint(request_obj, &block)
endpoint = Rack::OAuth2::Server::Authorize.new &block
req = {
Rack::RACK_REQUEST_QUERY_HASH => request_obj,
Rack::RACK_REQUEST_QUERY_STRING => "X",
Rack::QUERY_STRING => "X",
}
# 戻り値 = [ 200, {"Content-Type" => "text/plain"}, ["Hello Rack!\n\n"] ]
status, header, res_body = endpoint.call(req)

require_login() # 再ログインでここに戻ってくる. OK
if !allow_approval
if (max_age = @request_object.try(:id_token).try(:max_age)) &&
current_user.last_login_at < max_age.seconds.ago
flash[:alert] = 'Exceeded Max Age, Login Again'
logout()
require_login()
end
end

# エラー時に、次のようにレスポンスヘッダに格納する
# エラー時に、次のようにレスポンスヘッダに格納する. Status は上のほうの
# `rescue_from` で set される.
# HTTP/1.1 401 Unauthorized
# WWW-Authenticate: Bearer realm="example",
# error="invalid_token",
Expand All @@ -60,25 +131,20 @@ def call_authorization_endpoint allow_approval, approved = false
end


# @param allow_approval RPからのリダイレクトの時 false
# ユーザによる approve/deny のとき true
# @param approved ユーザによる approve のとき true.
#
# @return [Rack::OAuth2::Server::Authorize] rackオブジェクト.
def authorization_endpoint_authenticator allow_approval, approved = false
return Rack::OAuth2::Server::Authorize.new do |req, res|
raise TypeError if !req.is_a?(Rack::OAuth2::Server::Authorize::Request)
raise TypeError if !res.is_a?(Rack::OAuth2::Server::Authorize::Response)

@client = Client.find_by_identifier(req.client_id) || req.bad_request!
res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)
if res.protocol_params_location == :fragment && req.nonce.blank?
req.invalid_request! 'nonce required'
end
# req.scope は配列.
openid_scope_value = false
@scopes = req.scope.inject([]) do |_scopes_, scope|
openid_scope_value = true if scope == 'openid'
# 当初リダイレクト時に callback
# View で表示するため, 次を設定:
# @client, @redirect_uri, @scopes
def request_validation req, res
response_type = Array(req.response_type).collect(&:to_s).sort().join(' ')
if !Client.available_response_types.include?(response_type)
raise Rack::OAuth2::Server::Authorize::BadRequest.new("unsupported `response_type`")
end

@client = Client.find_by_identifier(req.client_id) || req.bad_request!
@redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)

# req.scope は配列.
@scopes = req.scope.inject([]) do |_scopes_, scope|
_scope_ = Scope.find_by_name(scope)
if _scope_
_scopes_ << _scope_
Expand All @@ -87,96 +153,92 @@ def authorization_endpoint_authenticator allow_approval, approved = false
# req.invalid_scope! "Unknown scope: #{scope}")
end
_scopes_
end
if !openid_scope_value
req.invalid_request! '`openid` scope value required'
end
@request_object =
if (@_request_ = req.request).present?
OpenIDConnect::RequestObject.decode req.request, nil # @client.secret
elsif (@request_uri = req.request_uri).present?
OpenIDConnect::RequestObject.fetch req.request_uri, nil # @client.secret
end

# req.response_type = :code ... Symbol
if Client.available_response_types.include?(
Array(req.response_type).collect(&:to_s).sort().join(' '))
if allow_approval
# ユーザによる approve/deny
if approved
approved! req, res
else
req.access_denied!
end
else
# 当初リダイレクト時
@response_type = req.response_type
end
else
req.unsupported_response_type!
end
# implicit, hybrid: `nonce` 必須.
# FAPI: `openid` scope を要求した場合は `nonce` 必須.
if (res.protocol_params_location == :fragment || Scope.ary_find(@scopes, 'openid')) &&
req.nonce.blank?
req.invalid_request! 'nonce required'
end

if !Scope.ary_find(@scopes, 'openid')
req.invalid_request! '`openid` scope value required'
end

# PKCE
if Array(req.response_type).include? :code
if req.code_challenge_method != "S256"
req.invalid_request!('only support `S256`')
end
end
end


def approved!(req, res)
# 'code', # Authorization Code Flow
# 'id_token token', # Implicit Flow
# 'code token', # Hybrid Flow
# 'code id_token', # Hybrid Flow
# 'code id_token token' # Hybrid Flow
# 'code', # Authorization Code Flow 認可コード
# 'id_token token', # Implicit Flow IDトークン + アクセストークン #fragment. エラーも fragment で
# 'code token', # Hybrid Flow 認可コード + アクセストークン #fragment. エラーも fragment で
# 'code id_token', # Hybrid Flow 認可コード + IDトークン #fragment. エラーも fragment で.
def consent_and_redirect_back(req, res)
response_types = Array(req.response_type)
fake_user = FakeUser.find params[:fake_user]
redirect_uri = req.verify_redirect_uri!(@request_object.client.redirect_uris)

if response_types.include? :code
# Authentication Response では code (と state) しか返さない.
# => この時点で, どのユーザか特定して保存が必要.
authorization = Authorization.create!(
fake_user: fake_user,
client: @client,
redirect_uri: res.redirect_uri,
ActiveRecord::Base.transaction do
authorization = Authorization.new(
fake_user: @fake_user,
client: @request_object.client,
redirect_uri: redirect_uri,
code_challenge: req.code_challenge, # PKCE
nonce: req.nonce)
authorization.scopes << @scopes # ユーザ (fake_user) が認可した scope
if @request_object
authorization.create_authorization_request_object!(
request_object: RequestObject.new(
jwt_string: @request_object.to_jwt(@client.secret, :HS256)
)
)
if req.claims && req.claims.length > 0
@request_object.save!
authorization.request_object_id = @request_object.id
end
authorization.save!
authorization.scopes << @authorized_scopes # ユーザ (fake_user) が認可した scope
end
res.code = authorization.code
end

# 事前検査済みなので、これでよい.
if response_types.include? :token
access_token = AccessToken.create!(fake_user:fake_user, client:@client)
access_token.scopes << @scopes
if @request_object
access_token.create_access_token_request_object!(
request_object: RequestObject.new(
jwt_string: @request_object.to_jwt(@client.secret, :HS256)
)
)
ActiveRecord::Base.transaction do
access_token = AccessToken.new(fake_user: @fake_user,
client: @request_object.client)
if req.claims && req.claims.length > 0
if req.claims.userinfo
@request_object.save!
access_token.request_object_id = @request_object.id
end
end
access_token.save!
access_token.scopes << @authorized_scopes
end
res.access_token = access_token.to_bearer_token
end

if response_types.include? :id_token
_id_token_ = IdToken.create!(fake_user:fake_user,
client: @client,
nonce: req.nonce )
if @request_object
_id_token_.create_id_token_request_object!(
request_object: RequestObject.new(
jwt_string: @request_object.to_jwt(@client.secret, :HS256)
)
)
ActiveRecord::Base.transaction do
_id_token_ = IdToken.new(fake_user: @fake_user,
client: @request_object.client,
nonce: req.nonce )
if req.claims && req.claims.length > 0
if req.claims.id_token
@request_object.save!
_id_token_.request_object_id = @request_object.id
end
end
_id_token_.save!
end

res.id_token = _id_token_.to_jwt(
code: (res.respond_to?(:code) ? res.code : nil),
access_token: (res.respond_to?(:access_token) ? res.access_token : nil)
)
end

res.approve!
end

Expand Down
7 changes: 4 additions & 3 deletions app/controllers/discovery_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,16 @@ def openid_configuration
# request object
request_parameter_supported: true,
request_uri_parameter_supported: true,
request_object_signing_alg_values_supported: [:HS256, :HS384, :HS512],
request_object_signing_alg_values_supported:
RequestObject::SIGNING_ALG_VALUES_SUPPORTED,

subject_types_supported: ['public', 'pairwise'],
id_token_signing_alg_values_supported: [:RS256],
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'],
claims_supported: ['sub', 'iss',
'name', 'email', 'address', 'profile', 'locale', 'phone_number'],
'name', 'email', 'email_verified', 'address', 'profile', 'locale', 'phone_number'],
# PKCE
code_challenge_methods_supported: ['S256']
code_challenge_methods_supported: ['S256'] ●●これが表示されない
)
render json: config
end
Expand Down
7 changes: 5 additions & 2 deletions app/controllers/tokens_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

# Token Endpoint
# The Authorization Code Flow: アクセストークンと id token の二つを返す.
class TokensController < ApplicationController
protect_from_forgery with: :null_session
class TokensController < ApiController
#protect_from_forgery with: :null_session

# POST /access_tokens
def index
Expand All @@ -30,6 +30,9 @@ def token_endpoint
if !authorization || !authorization.valid_redirect_uri?(req.redirect_uri)
req.invalid_grant!
end
# PKCE
req.verify_code_verifier!(authorization.code_challenge)

access_token = authorization.access_token
res.access_token = access_token.to_bearer_token
if access_token.accessible?(Scope::OPENID)
Expand Down
1 change: 0 additions & 1 deletion app/models/access_token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class AccessToken < ApplicationRecord

before_validation :setup, on: :create

#validates :client, presence: true
validates :token, presence: true, uniqueness: true
validates :expires_at, presence: true

Expand Down
4 changes: 1 addition & 3 deletions app/models/authorization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ class Authorization < ApplicationRecord

#has_one :authorization_request_object
# "claims" リクエストパラメータがあった場合, アクセストークン・id token の
# レスポンスに含めるクレームを削る
# レスポンスに含めるクレームを限定できる
belongs_to :request_object, optional:true #through: :authorization_request_object

before_validation :setup, on: :create

#validates :client, presence: true
#validates :fake_user, presence: true
validates :code, presence: true, uniqueness: true
validates :expires_at, presence: true

Expand Down
Loading

0 comments on commit e8bbd20

Please sign in to comment.