From 17fde49d61c2cb8f77724f6c1b937e11101ac67b Mon Sep 17 00:00:00 2001 From: theworldbright Date: Wed, 15 Jul 2015 14:17:21 +0900 Subject: [PATCH] Implement ID Token for the implicit flow --- .../authorizations_controller.rb | 46 ++++++++++++------- .../openid_connect/discovery_controller.rb | 4 +- .../openid_connect/id_tokens_controller.rb | 5 ++ app/models/id_token.rb | 38 +++++++++++++++ app/models/user.rb | 1 + .../authorizations/new.html.haml | 2 +- config/routes.rb | 1 + db/migrate/20150714055110_create_id_tokens.rb | 12 +++++ db/schema.rb | 14 +++++- lib/account_deleter.rb | 2 +- lib/openid_connect/endpoints/endpoint.rb | 13 +++--- .../endpoints/endpoint_confirmation_point.rb | 5 +- .../endpoints/endpoint_start_point.rb | 4 -- lib/openid_connect/id_token_config.rb | 11 +++++ .../protected_resource_endpoint.rb | 10 +--- .../authorizations_controller_spec.rb | 6 ++- .../id_tokens_controller_spec.rb | 19 ++++++++ spec/requests/api/v2/base_controller_spec.rb | 4 -- 18 files changed, 150 insertions(+), 47 deletions(-) create mode 100644 app/controllers/openid_connect/id_tokens_controller.rb create mode 100644 app/models/id_token.rb create mode 100644 db/migrate/20150714055110_create_id_tokens.rb create mode 100644 lib/openid_connect/id_token_config.rb create mode 100644 spec/controllers/openid_connect/id_tokens_controller_spec.rb delete mode 100644 spec/requests/api/v2/base_controller_spec.rb diff --git a/app/controllers/openid_connect/authorizations_controller.rb b/app/controllers/openid_connect/authorizations_controller.rb index e5f39e8b0..46f226fb2 100644 --- a/app/controllers/openid_connect/authorizations_controller.rb +++ b/app/controllers/openid_connect/authorizations_controller.rb @@ -17,7 +17,7 @@ class OpenidConnect::AuthorizationsController < ApplicationController private def request_authorization_consent_form - endpoint = OpenidConnect::Endpoints::EndpointStartPoint.new(current_user) + endpoint = OpenidConnect::Authorization::EndpointStartPoint.new(current_user) handle_startpoint_response(endpoint) end @@ -26,28 +26,28 @@ class OpenidConnect::AuthorizationsController < ApplicationController if response.redirect? redirect_to header["Location"] else - @client, @response_type, @redirect_uri, @scopes, @request_object = *[ - endpoint.client, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint.request_object - ] - save_request_parameters - render :new + saveParamsAndRenderConsentForm(endpoint) end end def process_authorization_consent(approvedString) - endpoint = OpenidConnect::Endpoints::EndpointConfirmationPoint.new(current_user, to_boolean(approvedString)) - restore_request_parameters(endpoint) + endpoint = OpenidConnect::Authorization::EndpointConfirmationPoint.new(current_user, to_boolean(approvedString)) handle_confirmation_endpoint_response(endpoint) end - def handle_confirmation_endpoint_response(endpoint) - _status, header, _response = *endpoint.call(request.env) - redirect_to header["Location"] + def saveParamsAndRenderConsentForm(endpoint) + @o_auth_application, @response_type, @redirect_uri, @scopes, @request_object = *[ + endpoint.o_auth_application, endpoint.response_type, endpoint.redirect_uri, endpoint.scopes, endpoint.request_object + ] + save_request_parameters + render :new end - def save_request_parameters - session[:client_id], session[:response_type], session[:redirect_uri], session[:scopes], session[:request_object] = - @client.client_id, @response_type, @redirect_uri, @scopes.map(&:name), @request_object + def handle_confirmation_endpoint_response(endpoint) + restore_request_parameters(endpoint) + _status, header, _response = *endpoint.call(request.env) + delete_authorization_session_variables + redirect_to header["Location"] end def restore_request_parameters(endpoint) @@ -55,8 +55,22 @@ class OpenidConnect::AuthorizationsController < ApplicationController 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].map {|scope| OpenidConnect::Scope.find_by_name(scope) }, session[:request_object] + endpoint.scopes, endpoint.request_object, endpoint.nonce = + 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 def to_boolean(str) diff --git a/app/controllers/openid_connect/discovery_controller.rb b/app/controllers/openid_connect/discovery_controller.rb index c06678fa3..d2b6745f1 100644 --- a/app/controllers/openid_connect/discovery_controller.rb +++ b/app/controllers/openid_connect/discovery_controller.rb @@ -17,14 +17,14 @@ class OpenidConnect::DiscoveryController < ApplicationController authorization_endpoint: new_openid_connect_authorization_url, token_endpoint: openid_connect_access_tokens_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), response_types_supported: OAuthApplication.available_response_types, request_object_signing_alg_values_supported: %i(HS256 HS384 HS512), subject_types_supported: %w(public pairwise), id_token_signing_alg_values_supported: %i(RS256), token_endpoint_auth_methods_supported: %w(client_secret_basic client_secret_post), - # TODO: claims_supported: ["sub", "iss", "name", "email"] + # TODO: claims_supported: ["sub", "iss", "name", "email"] ) end end diff --git a/app/controllers/openid_connect/id_tokens_controller.rb b/app/controllers/openid_connect/id_tokens_controller.rb new file mode 100644 index 000000000..75e0d69b4 --- /dev/null +++ b/app/controllers/openid_connect/id_tokens_controller.rb @@ -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 diff --git a/app/models/id_token.rb b/app/models/id_token.rb new file mode 100644 index 000000000..fc65b3751 --- /dev/null +++ b/app/models/id_token.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 28fa845a2..88612b66d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,6 +79,7 @@ class User < ActiveRecord::Base has_many :authorizations, class_name: "OpenidConnect::Authorization" 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 :id_tokens, class_name: "OpenidConnect::IdToken" before_save :guard_unconfirmed_email, :save_person! diff --git a/app/views/openid_connect/authorizations/new.html.haml b/app/views/openid_connect/authorizations/new.html.haml index 31726c0fd..c134be8a7 100644 --- a/app/views/openid_connect/authorizations/new.html.haml +++ b/app/views/openid_connect/authorizations/new.html.haml @@ -1,4 +1,4 @@ -%h2= @client.name +%h2= @o_auth_application.name %p= t(".will_be_redirected") = @redirect_uri = t(".with_id_token") diff --git a/config/routes.rb b/config/routes.rb index 79021f7cd..e1c9bdf2e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -245,6 +245,7 @@ Diaspora::Application.routes.draw do get ".well-known/webfinger", to: "discovery#webfinger" get ".well-known/openid-configuration", to: "discovery#configuration" + get "jwks.json", to: "id_tokens#jwks" end api_version(module: "Api::V0", path: {value: "api/v0"}, default: true) do diff --git a/db/migrate/20150714055110_create_id_tokens.rb b/db/migrate/20150714055110_create_id_tokens.rb new file mode 100644 index 000000000..0aadbe880 --- /dev/null +++ b/db/migrate/20150714055110_create_id_tokens.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 7b3993f06..2a7e29092 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # 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| 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 + 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| t.string "token", limit: 255 t.integer "user_id", limit: 4 diff --git a/lib/account_deleter.rb b/lib/account_deleter.rb index 63d17fae5..c102cb6c0 100644 --- a/lib/account_deleter.rb +++ b/lib/account_deleter.rb @@ -47,7 +47,7 @@ class AccountDeleter #user deletions def normal_ar_user_associates_to_delete %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 def special_ar_user_associations diff --git a/lib/openid_connect/endpoints/endpoint.rb b/lib/openid_connect/endpoints/endpoint.rb index 8bd813102..9a1b35eca 100644 --- a/lib/openid_connect/endpoints/endpoint.rb +++ b/lib/openid_connect/endpoints/endpoint.rb @@ -1,16 +1,15 @@ module OpenidConnect - module Endpoints + module Authorization class Endpoint - attr_accessor :app, :user, :client, :redirect_uri, :response_type, - :scopes, :_request_, :request_uri, :request_object + attr_accessor :app, :user, :o_auth_application, :redirect_uri, :response_type, + :scopes, :_request_, :request_uri, :request_object, :nonce delegate :call, to: :app def initialize(current_user) @user = current_user @app = Rack::OAuth2::Server::Authorize.new do |req, res| build_attributes(req, res) - if OpenidConnect::OAuthApplication.available_response_types.include?( - Array(req.response_type).map(&:to_s).join(" ")) + if OAuthApplication.available_response_types.include? Array(req.response_type).map(&:to_s).join(" ") handle_response_type(req, res) else req.unsupported_response_type! @@ -30,11 +29,11 @@ module OpenidConnect private 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 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 diff --git a/lib/openid_connect/endpoints/endpoint_confirmation_point.rb b/lib/openid_connect/endpoints/endpoint_confirmation_point.rb index fcb666aec..2400d6db1 100644 --- a/lib/openid_connect/endpoints/endpoint_confirmation_point.rb +++ b/lib/openid_connect/endpoints/endpoint_confirmation_point.rb @@ -26,7 +26,10 @@ module OpenidConnect 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 + 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 res.approve! end diff --git a/lib/openid_connect/endpoints/endpoint_start_point.rb b/lib/openid_connect/endpoints/endpoint_start_point.rb index 51f1786c4..0a167578f 100644 --- a/lib/openid_connect/endpoints/endpoint_start_point.rb +++ b/lib/openid_connect/endpoints/endpoint_start_point.rb @@ -1,10 +1,6 @@ module OpenidConnect module Endpoints class EndpointStartPoint < Endpoint - def initialize(current_user) - super(current_user) - end - def handle_response_type(req, res) @response_type = req.response_type end diff --git a/lib/openid_connect/id_token_config.rb b/lib/openid_connect/id_token_config.rb new file mode 100644 index 000000000..cac535c03 --- /dev/null +++ b/lib/openid_connect/id_token_config.rb @@ -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 diff --git a/lib/openid_connect/protected_resource_endpoint.rb b/lib/openid_connect/protected_resource_endpoint.rb index 4b7a7ea9c..8d5a37165 100644 --- a/lib/openid_connect/protected_resource_endpoint.rb +++ b/lib/openid_connect/protected_resource_endpoint.rb @@ -1,14 +1,6 @@ module OpenidConnect module ProtectedResourceEndpoint - def self.included(klass) - klass.send :include, ProtectedResourceEndpoint::Helper - end - - module Helper - def current_token - @current_token - end - end + attr_reader :current_token def require_access_token @current_token = request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN] diff --git a/spec/controllers/openid_connect/authorizations_controller_spec.rb b/spec/controllers/openid_connect/authorizations_controller_spec.rb index 6bfe7d3da..a8f9d4f60 100644 --- a/spec/controllers/openid_connect/authorizations_controller_spec.rb +++ b/spec/controllers/openid_connect/authorizations_controller_spec.rb @@ -91,7 +91,7 @@ describe OpenidConnect::AuthorizationsController, type: :controller do 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: 418_093_098_3 + scope: "openid", nonce: 418_093_098_3, state: 418_093_098_3 end 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 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 it "should return the passed in state" do diff --git a/spec/controllers/openid_connect/id_tokens_controller_spec.rb b/spec/controllers/openid_connect/id_tokens_controller_spec.rb new file mode 100644 index 000000000..6739a3561 --- /dev/null +++ b/spec/controllers/openid_connect/id_tokens_controller_spec.rb @@ -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 diff --git a/spec/requests/api/v2/base_controller_spec.rb b/spec/requests/api/v2/base_controller_spec.rb deleted file mode 100644 index dc359bbe2..000000000 --- a/spec/requests/api/v2/base_controller_spec.rb +++ /dev/null @@ -1,4 +0,0 @@ -require "spec_helper" - -describe Api::V0::BaseController do -end