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|
|
rescue_from Rack::OAuth2::Server::Authorize::BadRequest do |e|
|
||||||
@error = e
|
@error = e
|
||||||
logger.info e.backtrace[0,10].join("\n")
|
print e.backtrace[0,10].join("\n")
|
||||||
render :error, status: e.status
|
render json: {error: :error, status: e.status} #error_message: e.message
|
||||||
end
|
end
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def new
|
def new
|
||||||
call_authorization_endpoint
|
request_authorization_consent_form
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
call_authorization_endpoint :is_create, params[:approve]
|
process_authorization_consent(params[:approve])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def call_authorization_endpoint(is_create = false, approved = false)
|
def request_authorization_consent_form
|
||||||
endpoint = AuthorizationEndpoint.new is_create, approved
|
endpoint = OpenidConnect::Authorization::EndpointStartPoint.new(current_user)
|
||||||
rack_response = *endpoint.call(request.env)
|
endpoint.call(request.env)
|
||||||
@client, @response_type, @redirect_uri, @scopes, @_request_, @request_uri, @request_object = *[
|
@client, @response_type, @redirect_uri, @scopes, @request_object = *[
|
||||||
endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint._request_, endpoint.request_uri, endpoint.request_object
|
endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint.request_object
|
||||||
]
|
]
|
||||||
if (
|
saveRequestParameters
|
||||||
!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
|
|
||||||
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
|
render :new
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ class OAuthApplication < ActiveRecord::Base
|
||||||
validates :client_id, presence: true, uniqueness: true
|
validates :client_id, presence: true, uniqueness: true
|
||||||
validates :client_secret, presence: true
|
validates :client_secret, presence: true
|
||||||
|
|
||||||
|
serialize :redirect_uris, JSON
|
||||||
|
|
||||||
before_validation :setup, on: :create
|
before_validation :setup, on: :create
|
||||||
def setup
|
def setup
|
||||||
self.client_id = SecureRandom.hex(16)
|
self.client_id = SecureRandom.hex(16)
|
||||||
|
|
@ -13,6 +15,10 @@ class OAuthApplication < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
def available_response_types
|
||||||
|
["id_token"]
|
||||||
|
end
|
||||||
|
|
||||||
def register!(registrarHash)
|
def register!(registrarHash)
|
||||||
registrarHash.validate!
|
registrarHash.validate!
|
||||||
buildClientApplication(registrarHash)
|
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)
|
def initialize(current_user)
|
||||||
@user = current_user
|
@user = current_user
|
||||||
@app = Rack::OAuth2::Server::Authorize.new do |req, res|
|
@app = Rack::OAuth2::Server::Authorize.new do |req, res|
|
||||||
buildClient(req)
|
buildAttributes(req, res)
|
||||||
buildRedirectURI(req, res)
|
|
||||||
verifyNonce(req, res)
|
|
||||||
buildScopes(req)
|
|
||||||
buildRequestObject(req)
|
|
||||||
if OAuthApplication.available_response_types.include? Array(req.response_type).collect(&:to_s).join(' ')
|
if OAuthApplication.available_response_types.include? Array(req.response_type).collect(&:to_s).join(' ')
|
||||||
handleResponseType(req, res)
|
handleResponseType(req, res)
|
||||||
else
|
else
|
||||||
|
|
@ -19,32 +15,23 @@ module OpenidConnect
|
||||||
end
|
end
|
||||||
end
|
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)
|
def buildClient(req)
|
||||||
@client = OAuthApplication.find_by_client_id(req.client_id) || req.bad_request!
|
@client = OAuthApplication.find_by_client_id(req.client_id) || req.bad_request!
|
||||||
end
|
end
|
||||||
def buildRedirectURI(req, res)
|
def buildRedirectURI(req, res)
|
||||||
res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)
|
res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)
|
||||||
end
|
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
|
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