Implement authorization endpoint (part 1)
The user can now authenticate with the authorization server's authorization endpoint and receive a fake id token.
This commit is contained in:
parent
059933f076
commit
3cfbcbce8f
8 changed files with 210 additions and 54 deletions
|
|
@ -1,47 +1,54 @@
|
|||
class AuthorizationsController < ApplicationController
|
||||
class OpenidConnect::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
|
||||
print e.backtrace[0,10].join("\n")
|
||||
render json: {error: :error, status: e.status} #error_message: e.message
|
||||
end
|
||||
|
||||
before_action :authenticate_user!
|
||||
|
||||
def new
|
||||
call_authorization_endpoint
|
||||
request_authorization_consent_form
|
||||
end
|
||||
|
||||
def create
|
||||
call_authorization_endpoint :is_create, params[:approve]
|
||||
process_authorization_consent(params[:approve])
|
||||
end
|
||||
|
||||
private
|
||||
private
|
||||
|
||||
def call_authorization_endpoint(is_create = false, approved = false)
|
||||
endpoint = AuthorizationEndpoint.new is_create, approved
|
||||
rack_response = *endpoint.call(request.env)
|
||||
@client, @response_type, @redirect_uri, @scopes, @_request_, @request_uri, @request_object = *[
|
||||
endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint._request_, endpoint.request_uri, endpoint.request_object
|
||||
def request_authorization_consent_form
|
||||
endpoint = OpenidConnect::Authorization::EndpointStartPoint.new(current_user)
|
||||
endpoint.call(request.env)
|
||||
@client, @response_type, @redirect_uri, @scopes, @request_object = *[
|
||||
endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint.request_object
|
||||
]
|
||||
if (
|
||||
!is_create &&
|
||||
(max_age = @request_object.try(:id_token).try(:max_age)) &&
|
||||
current_account.last_logged_in_at < max_age.seconds.ago
|
||||
)
|
||||
flash[:notice] = 'Exceeded Max Age, Login Again'
|
||||
unauthenticate!
|
||||
end
|
||||
respond_as_rack_app *rack_response
|
||||
saveRequestParameters
|
||||
render :new
|
||||
end
|
||||
|
||||
def respond_as_rack_app(status, header, response)
|
||||
["WWW-Authenticate"].each do |key|
|
||||
headers[key] = header[key] if header[key].present?
|
||||
end
|
||||
if response.redirect?
|
||||
redirect_to header['Location']
|
||||
else
|
||||
render :new
|
||||
end
|
||||
def process_authorization_consent(approvedString)
|
||||
endpoint = OpenidConnect::Authorization::EndpointConfirmationPoint.new(current_user, to_boolean(approvedString))
|
||||
restoreRequestParameters(endpoint)
|
||||
status, header, response = *endpoint.call(request.env)
|
||||
redirect_to header['Location']
|
||||
end
|
||||
|
||||
def saveRequestParameters
|
||||
session[:client_id], session[:response_type], session[:redirect_uri], session[:scopes], session[:request_object] =
|
||||
@client.client_id, @response_type, @redirect_uri, @scopes.collect { |scope| scope.name }, @request_object
|
||||
end
|
||||
|
||||
def restoreRequestParameters(endpoint)
|
||||
req = Rack::Request.new(request.env)
|
||||
req.update_param("client_id", session[:client_id])
|
||||
req.update_param("redirect_uri", session[:redirect_uri])
|
||||
req.update_param("response_type", session[:response_type])
|
||||
endpoint.scopes, endpoint.request_object =
|
||||
session[:scopes].collect {|scope| Scope.find_by_name(scope)}, session[:request_object]
|
||||
end
|
||||
|
||||
def to_boolean(str)
|
||||
str.downcase == "true"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ class OAuthApplication < ActiveRecord::Base
|
|||
validates :client_id, presence: true, uniqueness: true
|
||||
validates :client_secret, presence: true
|
||||
|
||||
serialize :redirect_uris, JSON
|
||||
|
||||
before_validation :setup, on: :create
|
||||
def setup
|
||||
self.client_id = SecureRandom.hex(16)
|
||||
|
|
@ -13,6 +15,10 @@ class OAuthApplication < ActiveRecord::Base
|
|||
end
|
||||
|
||||
class << self
|
||||
def available_response_types
|
||||
["id_token"]
|
||||
end
|
||||
|
||||
def register!(registrarHash)
|
||||
registrarHash.validate!
|
||||
buildClientApplication(registrarHash)
|
||||
|
|
|
|||
9
app/views/openid_connect/authorizations/_form.html.erb
Normal file
9
app/views/openid_connect/authorizations/_form.html.erb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<%= form_tag openid_connect_authorizations_path, class: action do %>
|
||||
<% if action == :approve %>
|
||||
<%= submit_tag "Approve" %>
|
||||
<%= hidden_field_tag :approve, true %>
|
||||
<% else %>
|
||||
<%= submit_tag "Deny" %>
|
||||
<%= hidden_field_tag :approve, false %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
15
app/views/openid_connect/authorizations/new.html.erb
Normal file
15
app/views/openid_connect/authorizations/new.html.erb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<h2><%= @client.name %></h2>
|
||||
<p>You will be redirected to <%= @redirect_uri %> with an id token if approved or an error if denied</p>
|
||||
<ul>
|
||||
<% @scopes.each do |scope| %>
|
||||
<li><%= scope.name %></li>
|
||||
<% end %>
|
||||
<% if @request_object %>
|
||||
<li>Request Objects (Currently not supported)</li>
|
||||
<ul>
|
||||
<pre><%= JSON.pretty_generate @request_object.as_json %></pre>
|
||||
</ul>
|
||||
<% end %>
|
||||
</ul>
|
||||
<%= render 'openid_connect/authorizations/form', action: :approve %>
|
||||
<%= render 'openid_connect/authorizations/form', action: :deny %>
|
||||
|
|
@ -7,11 +7,7 @@ module OpenidConnect
|
|||
def initialize(current_user)
|
||||
@user = current_user
|
||||
@app = Rack::OAuth2::Server::Authorize.new do |req, res|
|
||||
buildClient(req)
|
||||
buildRedirectURI(req, res)
|
||||
verifyNonce(req, res)
|
||||
buildScopes(req)
|
||||
buildRequestObject(req)
|
||||
buildAttributes(req, res)
|
||||
if OAuthApplication.available_response_types.include? Array(req.response_type).collect(&:to_s).join(' ')
|
||||
handleResponseType(req, res)
|
||||
else
|
||||
|
|
@ -19,32 +15,23 @@ module OpenidConnect
|
|||
end
|
||||
end
|
||||
end
|
||||
def buildAttributes(req, res)
|
||||
buildClient(req)
|
||||
buildRedirectURI(req, res)
|
||||
end
|
||||
|
||||
def handleResponseType(req, res)
|
||||
# Implemented by subclass
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def buildClient(req)
|
||||
@client = OAuthApplication.find_by_client_id(req.client_id) || req.bad_request!
|
||||
end
|
||||
def buildRedirectURI(req, res)
|
||||
res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)
|
||||
end
|
||||
def verifyNonce(req, res)
|
||||
if res.protocol_params_location == :fragment && req.nonce.blank?
|
||||
req.invalid_request! 'nonce required'
|
||||
end
|
||||
end
|
||||
def buildScopes(req)
|
||||
@scopes = req.scope.inject([]) do |_scopes_, scope|
|
||||
_scopes_ << Scope.find_by_name(scope) or req.invalid_scope! "Unknown scope: #{scope}"
|
||||
end
|
||||
end
|
||||
def buildRequestObject(req)
|
||||
@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
|
||||
end
|
||||
def handleResponseType(req, res)
|
||||
# Implemented by subclass
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
module OpenidConnect
|
||||
module Authorization
|
||||
class EndpointConfirmationPoint < Endpoint
|
||||
def initialize(current_user, approved = false)
|
||||
super(current_user)
|
||||
@approved = approved
|
||||
end
|
||||
|
||||
def buildAttributes(req, res)
|
||||
super(req, res)
|
||||
# TODO: buildResponseType(req)
|
||||
end
|
||||
|
||||
def handleResponseType(req, res)
|
||||
handleApproval(@approved, req, res)
|
||||
end
|
||||
|
||||
def handleApproval(approved, req, res)
|
||||
if approved
|
||||
approved!(req, res)
|
||||
else
|
||||
req.access_denied!
|
||||
end
|
||||
end
|
||||
|
||||
def approved!(req, res)
|
||||
response_types = Array(req.response_type)
|
||||
if response_types.include?(:id_token)
|
||||
res.id_token = SecureRandom.hex(16) # TODO: Replace with real ID token
|
||||
end
|
||||
res.approve!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
lib/openid_connect/authorization/endpoint_start_point.rb
Normal file
28
lib/openid_connect/authorization/endpoint_start_point.rb
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
module OpenidConnect
|
||||
module Authorization
|
||||
class EndpointStartPoint < Endpoint
|
||||
def initialize(current_user)
|
||||
super(current_user)
|
||||
end
|
||||
def handleResponseType(req, res)
|
||||
@response_type = req.response_type
|
||||
end
|
||||
def buildAttributes(req, res)
|
||||
super(req, res)
|
||||
verifyNonce(req, res)
|
||||
buildScopes(req)
|
||||
# TODO: buildRequestObject(req)
|
||||
end
|
||||
def verifyNonce(req, res)
|
||||
if res.protocol_params_location == :fragment && req.nonce.blank?
|
||||
req.invalid_request! "nonce required"
|
||||
end
|
||||
end
|
||||
def buildScopes(req)
|
||||
@scopes = req.scope.inject([]) do |_scopes_, scope|
|
||||
_scopes_ << Scope.find_by_name(scope) or req.invalid_scope! "Unknown scope: #{scope}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe OpenidConnect::AuthorizationsController, type: :controller do
|
||||
let!(:client) { OAuthApplication.create!(redirect_uris: ["http://localhost:3000/"]) }
|
||||
|
||||
before do
|
||||
sign_in :user, alice
|
||||
allow(@controller).to receive(:current_user).and_return(alice)
|
||||
Scope.create!(name:"openid")
|
||||
end
|
||||
|
||||
describe "#new" do
|
||||
render_views
|
||||
context "when valid parameters are passed" do
|
||||
it "should return a form page" do
|
||||
get :new,
|
||||
{
|
||||
client_id: client.client_id,
|
||||
redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token",
|
||||
scope: "openid",
|
||||
nonce: SecureRandom.hex(16),
|
||||
state: SecureRandom.hex(16)
|
||||
}
|
||||
expect(response.body).to match("Approve")
|
||||
expect(response.body).to match("Deny")
|
||||
end
|
||||
end
|
||||
# TODO: Implement tests for missing/invalid parameters
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
before do
|
||||
get :new,
|
||||
{
|
||||
client_id: client.client_id,
|
||||
redirect_uri: "http://localhost:3000/",
|
||||
response_type: "id_token",
|
||||
scope: "openid",
|
||||
nonce: SecureRandom.hex(16),
|
||||
state: SecureRandom.hex(16)
|
||||
}
|
||||
end
|
||||
context "when authorization is approved" do
|
||||
it "should return the id token in a fragment" do
|
||||
post :create,
|
||||
{
|
||||
approve: "true"
|
||||
}
|
||||
expect(response.location).to have_content("#id_token=")
|
||||
end
|
||||
end
|
||||
context "when authorization is denied" do
|
||||
before do
|
||||
post :create,
|
||||
{
|
||||
approve: "false"
|
||||
}
|
||||
end
|
||||
it "should return an error in the fragment" do
|
||||
expect(response.location).to have_content("#error=")
|
||||
end
|
||||
it "should NOT contain a id token in the fragment" do
|
||||
expect(response.location).to_not have_content("#id_token=")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
Loading…
Reference in a new issue