Implement ID Token for the implicit flow
This commit is contained in:
parent
1475672d72
commit
17fde49d61
18 changed files with 150 additions and 47 deletions
|
|
@ -17,7 +17,7 @@ class OpenidConnect::AuthorizationsController < ApplicationController
|
||||||
private
|
private
|
||||||
|
|
||||||
def request_authorization_consent_form
|
def request_authorization_consent_form
|
||||||
endpoint = OpenidConnect::Endpoints::EndpointStartPoint.new(current_user)
|
endpoint = OpenidConnect::Authorization::EndpointStartPoint.new(current_user)
|
||||||
handle_startpoint_response(endpoint)
|
handle_startpoint_response(endpoint)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -26,28 +26,28 @@ class OpenidConnect::AuthorizationsController < ApplicationController
|
||||||
if response.redirect?
|
if response.redirect?
|
||||||
redirect_to header["Location"]
|
redirect_to header["Location"]
|
||||||
else
|
else
|
||||||
@client, @response_type, @redirect_uri, @scopes, @request_object = *[
|
saveParamsAndRenderConsentForm(endpoint)
|
||||||
endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint.request_object
|
|
||||||
]
|
|
||||||
save_request_parameters
|
|
||||||
render :new
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_authorization_consent(approvedString)
|
def process_authorization_consent(approvedString)
|
||||||
endpoint = OpenidConnect::Endpoints::EndpointConfirmationPoint.new(current_user, to_boolean(approvedString))
|
endpoint = OpenidConnect::Authorization::EndpointConfirmationPoint.new(current_user, to_boolean(approvedString))
|
||||||
restore_request_parameters(endpoint)
|
|
||||||
handle_confirmation_endpoint_response(endpoint)
|
handle_confirmation_endpoint_response(endpoint)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_confirmation_endpoint_response(endpoint)
|
def saveParamsAndRenderConsentForm(endpoint)
|
||||||
_status, header, _response = *endpoint.call(request.env)
|
@o_auth_application, @response_type, @redirect_uri, @scopes, @request_object = *[
|
||||||
redirect_to header["Location"]
|
endpoint.o_auth_application, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint.request_object
|
||||||
|
]
|
||||||
|
save_request_parameters
|
||||||
|
render :new
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_request_parameters
|
def handle_confirmation_endpoint_response(endpoint)
|
||||||
session[:client_id], session[:response_type], session[:redirect_uri], session[:scopes], session[:request_object] =
|
restore_request_parameters(endpoint)
|
||||||
@client.client_id, @response_type, @redirect_uri, @scopes.map(&:name), @request_object
|
_status, header, _response = *endpoint.call(request.env)
|
||||||
|
delete_authorization_session_variables
|
||||||
|
redirect_to header["Location"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def restore_request_parameters(endpoint)
|
def restore_request_parameters(endpoint)
|
||||||
|
|
@ -55,8 +55,22 @@ class OpenidConnect::AuthorizationsController < ApplicationController
|
||||||
req.update_param("client_id", session[:client_id])
|
req.update_param("client_id", session[:client_id])
|
||||||
req.update_param("redirect_uri", session[:redirect_uri])
|
req.update_param("redirect_uri", session[:redirect_uri])
|
||||||
req.update_param("response_type", session[:response_type])
|
req.update_param("response_type", session[:response_type])
|
||||||
endpoint.scopes, endpoint.request_object =
|
endpoint.scopes, endpoint.request_object, endpoint.nonce =
|
||||||
session[:scopes].map {|scope| OpenidConnect::Scope.find_by_name(scope) }, session[:request_object]
|
session[:scopes].map {|scope| Scope.find_by_name(scope) }, session[:request_object], session[:nonce]
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_authorization_session_variables
|
||||||
|
session.delete(:client_id)
|
||||||
|
session.delete(:response_type)
|
||||||
|
session.delete(:redirect_uri)
|
||||||
|
session.delete(:scopes)
|
||||||
|
session.delete(:request_object)
|
||||||
|
session.delete(:nonce)
|
||||||
|
end
|
||||||
|
|
||||||
|
def save_request_parameters
|
||||||
|
session[:client_id], session[:response_type], session[:redirect_uri], session[:scopes], session[:request_object], session[:nonce] =
|
||||||
|
@o_auth_application.client_id, @response_type, @redirect_uri, @scopes.map(&:name), @request_object, params[:nonce]
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_boolean(str)
|
def to_boolean(str)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class OpenidConnect::DiscoveryController < ApplicationController
|
||||||
authorization_endpoint: new_openid_connect_authorization_url,
|
authorization_endpoint: new_openid_connect_authorization_url,
|
||||||
token_endpoint: openid_connect_access_tokens_url,
|
token_endpoint: openid_connect_access_tokens_url,
|
||||||
userinfo_endpoint: api_v0_user_url,
|
userinfo_endpoint: api_v0_user_url,
|
||||||
jwks_uri: "https://not_configured_yet.com", # TODO: File.join({new_openid_connect_authorization_path} + "/jwks.json"),
|
jwks_uri: File.join(root_url, "openid_connect", "jwks.json"),
|
||||||
scopes_supported: Scope.pluck(:name),
|
scopes_supported: Scope.pluck(:name),
|
||||||
response_types_supported: OAuthApplication.available_response_types,
|
response_types_supported: OAuthApplication.available_response_types,
|
||||||
request_object_signing_alg_values_supported: %i(HS256 HS384 HS512),
|
request_object_signing_alg_values_supported: %i(HS256 HS384 HS512),
|
||||||
|
|
|
||||||
5
app/controllers/openid_connect/id_tokens_controller.rb
Normal file
5
app/controllers/openid_connect/id_tokens_controller.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
class OpenidConnect::IdTokensController < ApplicationController
|
||||||
|
def jwks
|
||||||
|
render json: JSON::JWK::Set.new(JSON::JWK.new(OpenidConnect::IdTokenConfig.public_key, use: :sig)).as_json
|
||||||
|
end
|
||||||
|
end
|
||||||
38
app/models/id_token.rb
Normal file
38
app/models/id_token.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
class IdToken < ActiveRecord::Base
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :o_auth_application
|
||||||
|
|
||||||
|
before_validation :setup, on: :create
|
||||||
|
|
||||||
|
validates :user, presence: true
|
||||||
|
validates :o_auth_application, presence: true
|
||||||
|
|
||||||
|
default_scope -> { where("expires_at >= ?", Time.now.utc) }
|
||||||
|
|
||||||
|
def setup
|
||||||
|
self.expires_at = 30.minutes.from_now
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_jwt(options = {})
|
||||||
|
to_response_object(options).to_jwt OpenidConnect::IdTokenConfig.private_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_response_object(options = {})
|
||||||
|
claims = {
|
||||||
|
iss: AppConfig.environment.url,
|
||||||
|
sub: AppConfig.environment.url + o_auth_application.client_id.to_s + user.id.to_s, # TODO: Convert to proper PPID
|
||||||
|
aud: o_auth_application.client_id,
|
||||||
|
exp: expires_at.to_i,
|
||||||
|
iat: created_at.to_i,
|
||||||
|
auth_time: user.current_sign_in_at.to_i,
|
||||||
|
nonce: nonce,
|
||||||
|
acr: 0 # TODO: Adjust ?
|
||||||
|
}
|
||||||
|
id_token = OpenIDConnect::ResponseObject::IdToken.new(claims)
|
||||||
|
id_token.code = options[:code] if options[:code]
|
||||||
|
id_token.access_token = options[:access_token] if options[:access_token]
|
||||||
|
id_token
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Add support for request objects
|
||||||
|
end
|
||||||
|
|
@ -79,6 +79,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :authorizations, class_name: "OpenidConnect::Authorization"
|
has_many :authorizations, class_name: "OpenidConnect::Authorization"
|
||||||
has_many :o_auth_applications, through: :authorizations, class_name: "OpenidConnect::OAuthApplication"
|
has_many :o_auth_applications, through: :authorizations, class_name: "OpenidConnect::OAuthApplication"
|
||||||
has_many :o_auth_access_tokens, through: :authorizations, class_name: "OpenidConnect::OAuthAccessToken"
|
has_many :o_auth_access_tokens, through: :authorizations, class_name: "OpenidConnect::OAuthAccessToken"
|
||||||
|
has_many :id_tokens, class_name: "OpenidConnect::IdToken"
|
||||||
|
|
||||||
before_save :guard_unconfirmed_email,
|
before_save :guard_unconfirmed_email,
|
||||||
:save_person!
|
:save_person!
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
%h2= @client.name
|
%h2= @o_auth_application.name
|
||||||
%p= t(".will_be_redirected")
|
%p= t(".will_be_redirected")
|
||||||
= @redirect_uri
|
= @redirect_uri
|
||||||
= t(".with_id_token")
|
= t(".with_id_token")
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,7 @@ Diaspora::Application.routes.draw do
|
||||||
|
|
||||||
get ".well-known/webfinger", to: "discovery#webfinger"
|
get ".well-known/webfinger", to: "discovery#webfinger"
|
||||||
get ".well-known/openid-configuration", to: "discovery#configuration"
|
get ".well-known/openid-configuration", to: "discovery#configuration"
|
||||||
|
get "jwks.json", to: "id_tokens#jwks"
|
||||||
end
|
end
|
||||||
|
|
||||||
api_version(module: "Api::V0", path: {value: "api/v0"}, default: true) do
|
api_version(module: "Api::V0", path: {value: "api/v0"}, default: true) do
|
||||||
|
|
|
||||||
12
db/migrate/20150714055110_create_id_tokens.rb
Normal file
12
db/migrate/20150714055110_create_id_tokens.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
class CreateIdTokens < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :id_tokens do |t|
|
||||||
|
t.belongs_to :user, index: true
|
||||||
|
t.belongs_to :o_auth_application, index: true
|
||||||
|
t.datetime :expires_at
|
||||||
|
t.string :nonce
|
||||||
|
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
14
db/schema.rb
14
db/schema.rb
|
|
@ -11,7 +11,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20150708155747) do
|
ActiveRecord::Schema.define(version: 20150714055110) do
|
||||||
|
|
||||||
create_table "account_deletions", force: :cascade do |t|
|
create_table "account_deletions", force: :cascade do |t|
|
||||||
t.string "diaspora_handle", limit: 255
|
t.string "diaspora_handle", limit: 255
|
||||||
|
|
@ -156,6 +156,18 @@ ActiveRecord::Schema.define(version: 20150708155747) do
|
||||||
|
|
||||||
add_index "conversations", ["author_id"], name: "conversations_author_id_fk", using: :btree
|
add_index "conversations", ["author_id"], name: "conversations_author_id_fk", using: :btree
|
||||||
|
|
||||||
|
create_table "id_tokens", force: :cascade do |t|
|
||||||
|
t.integer "user_id", limit: 4
|
||||||
|
t.integer "o_auth_application_id", limit: 4
|
||||||
|
t.datetime "expires_at"
|
||||||
|
t.string "nonce", limit: 255
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index "id_tokens", ["o_auth_application_id"], name: "index_id_tokens_on_o_auth_application_id", using: :btree
|
||||||
|
add_index "id_tokens", ["user_id"], name: "index_id_tokens_on_user_id", using: :btree
|
||||||
|
|
||||||
create_table "invitation_codes", force: :cascade do |t|
|
create_table "invitation_codes", force: :cascade do |t|
|
||||||
t.string "token", limit: 255
|
t.string "token", limit: 255
|
||||||
t.integer "user_id", limit: 4
|
t.integer "user_id", limit: 4
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ class AccountDeleter
|
||||||
#user deletions
|
#user deletions
|
||||||
def normal_ar_user_associates_to_delete
|
def normal_ar_user_associates_to_delete
|
||||||
%i(tag_followings invitations_to_me services aspects user_preferences
|
%i(tag_followings invitations_to_me services aspects user_preferences
|
||||||
notifications blocks authorizations o_auth_applications o_auth_access_tokens)
|
notifications blocks authorizations o_auth_applications o_auth_access_tokens id_tokens)
|
||||||
end
|
end
|
||||||
|
|
||||||
def special_ar_user_associations
|
def special_ar_user_associations
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
module OpenidConnect
|
module OpenidConnect
|
||||||
module Endpoints
|
module Authorization
|
||||||
class Endpoint
|
class Endpoint
|
||||||
attr_accessor :app, :user, :client, :redirect_uri, :response_type,
|
attr_accessor :app, :user, :o_auth_application, :redirect_uri, :response_type,
|
||||||
:scopes, :_request_, :request_uri, :request_object
|
:scopes, :_request_, :request_uri, :request_object, :nonce
|
||||||
delegate :call, to: :app
|
delegate :call, to: :app
|
||||||
|
|
||||||
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|
|
||||||
build_attributes(req, res)
|
build_attributes(req, res)
|
||||||
if OpenidConnect::OAuthApplication.available_response_types.include?(
|
if OAuthApplication.available_response_types.include? Array(req.response_type).map(&:to_s).join(" ")
|
||||||
Array(req.response_type).map(&:to_s).join(" "))
|
|
||||||
handle_response_type(req, res)
|
handle_response_type(req, res)
|
||||||
else
|
else
|
||||||
req.unsupported_response_type!
|
req.unsupported_response_type!
|
||||||
|
|
@ -30,11 +29,11 @@ module OpenidConnect
|
||||||
private
|
private
|
||||||
|
|
||||||
def build_client(req)
|
def build_client(req)
|
||||||
@client = OpenidConnect::OAuthApplication.find_by_client_id(req.client_id) || req.bad_request!
|
@o_auth_application = OAuthApplication.find_by_client_id(req.client_id) || req.bad_request!
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_redirect_uri(req, res)
|
def build_redirect_uri(req, res)
|
||||||
res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@client.redirect_uris)
|
res.redirect_uri = @redirect_uri = req.verify_redirect_uri!(@o_auth_application.redirect_uris)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ module OpenidConnect
|
||||||
def approved!(req, res)
|
def approved!(req, res)
|
||||||
response_types = Array(req.response_type)
|
response_types = Array(req.response_type)
|
||||||
if response_types.include?(:id_token)
|
if response_types.include?(:id_token)
|
||||||
res.id_token = SecureRandom.hex(16) # TODO: Replace with real ID token
|
id_token = @user.id_tokens.create!(o_auth_application: o_auth_application, nonce: @nonce)
|
||||||
|
options = %i(code access_token).map{|option| ["res.#{option}", res.respond_to?(option) ? res.option : nil]}.to_h
|
||||||
|
res.id_token = id_token.to_jwt(options)
|
||||||
|
# TODO: Add support for request object
|
||||||
end
|
end
|
||||||
res.approve!
|
res.approve!
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
module OpenidConnect
|
module OpenidConnect
|
||||||
module Endpoints
|
module Endpoints
|
||||||
class EndpointStartPoint < Endpoint
|
class EndpointStartPoint < Endpoint
|
||||||
def initialize(current_user)
|
|
||||||
super(current_user)
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_response_type(req, res)
|
def handle_response_type(req, res)
|
||||||
@response_type = req.response_type
|
@response_type = req.response_type
|
||||||
end
|
end
|
||||||
|
|
|
||||||
11
lib/openid_connect/id_token_config.rb
Normal file
11
lib/openid_connect/id_token_config.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
module OpenidConnect
|
||||||
|
class IdTokenConfig
|
||||||
|
@@key = OpenSSL::PKey::RSA.new(2048)
|
||||||
|
def self.public_key
|
||||||
|
@@key.public_key
|
||||||
|
end
|
||||||
|
def self.private_key
|
||||||
|
@@key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,14 +1,6 @@
|
||||||
module OpenidConnect
|
module OpenidConnect
|
||||||
module ProtectedResourceEndpoint
|
module ProtectedResourceEndpoint
|
||||||
def self.included(klass)
|
attr_reader :current_token
|
||||||
klass.send :include, ProtectedResourceEndpoint::Helper
|
|
||||||
end
|
|
||||||
|
|
||||||
module Helper
|
|
||||||
def current_token
|
|
||||||
@current_token
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_access_token
|
def require_access_token
|
||||||
@current_token = request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
|
@current_token = request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ describe OpenidConnect::AuthorizationsController, type: :controller do
|
||||||
describe "#create" do
|
describe "#create" do
|
||||||
before do
|
before do
|
||||||
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
get :new, client_id: client.client_id, redirect_uri: "http://localhost:3000/", response_type: "id_token",
|
||||||
scope: "openid", nonce: SecureRandom.hex(16), state: 418_093_098_3
|
scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when authorization is approved" do
|
context "when authorization is approved" do
|
||||||
|
|
@ -101,6 +101,10 @@ describe OpenidConnect::AuthorizationsController, type: :controller do
|
||||||
|
|
||||||
it "should return the id token in a fragment" do
|
it "should return the id token in a fragment" do
|
||||||
expect(response.location).to have_content("id_token=")
|
expect(response.location).to have_content("id_token=")
|
||||||
|
encoded_id_token = response.location[/(?<=id_token=)[^&]+/]
|
||||||
|
decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, OpenidConnect::IdTokenConfig.public_key
|
||||||
|
expect(decoded_token.nonce).to eq("4180930983")
|
||||||
|
expect(decoded_token.exp).to be > Time.now.utc.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should return the passed in state" do
|
it "should return the passed in state" do
|
||||||
|
|
|
||||||
19
spec/controllers/openid_connect/id_tokens_controller_spec.rb
Normal file
19
spec/controllers/openid_connect/id_tokens_controller_spec.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
require "spec_helper"
|
||||||
|
|
||||||
|
describe OpenidConnect::IdTokensController, type: :controller do
|
||||||
|
describe "#jwks" do
|
||||||
|
before do
|
||||||
|
get :jwks
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should contain a public key that matches the internal private key" do
|
||||||
|
json = JSON.parse(response.body).with_indifferent_access
|
||||||
|
jwks = JSON::JWK::Set.new json[:keys]
|
||||||
|
public_keys = jwks.collect do |jwk|
|
||||||
|
JSON::JWK.decode jwk
|
||||||
|
end
|
||||||
|
public_key = public_keys.first
|
||||||
|
expect(OpenidConnect::IdTokenConfig.private_key.public_key.to_s).to eq(public_key.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
require "spec_helper"
|
|
||||||
|
|
||||||
describe Api::V0::BaseController do
|
|
||||||
end
|
|
||||||
Loading…
Reference in a new issue