diff --git a/Gemfile b/Gemfile index 67cf53b1acb21b05089c95591e16e7da7ec574a5..2e4c4122ed2cda1635948a45ecd9a3ebedf89dbd 100644 --- a/Gemfile +++ b/Gemfile @@ -149,6 +149,9 @@ gem "omniauth-twitter", "1.2.1" gem "twitter", "5.15.0" gem "omniauth-wordpress", "0.2.2" +# OpenID Connect +gem "openid_connect" + # Serializers gem "active_model_serializers", "0.9.3" diff --git a/app/controllers/openid/authorizations_controller.rb b/app/controllers/openid/authorizations_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..f1d407043353d12d49a02ac2bcedbbc631933572 --- /dev/null +++ b/app/controllers/openid/authorizations_controller.rb @@ -0,0 +1,47 @@ +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 + end + + def new + call_authorization_endpoint + end + + def create + call_authorization_endpoint :allow_approval, params[:approve] + end + + private + + def call_authorization_endpoint(allow_approval = false, approved = false) + endpoint = AuthorizationEndpoint.new allow_approval, 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 + ] + require_authentication + if ( + !allow_approval && + (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! + require_authentication + 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 + end + end +end diff --git a/app/controllers/openid/connect_controller.rb b/app/controllers/openid/connect_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..c552988a6d2d29bc636b326a356773ebc9a4d75b --- /dev/null +++ b/app/controllers/openid/connect_controller.rb @@ -0,0 +1,4 @@ +class ConnectController < ApplicationController + def show + end +end diff --git a/app/controllers/openid/discovery_controller.rb b/app/controllers/openid/discovery_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..1b70dcae09dc585da0c702176bd8be3ca02a323a --- /dev/null +++ b/app/controllers/openid/discovery_controller.rb @@ -0,0 +1,45 @@ +class DiscoveryController < ApplicationController + def show + case params[:id] + when 'webfinger' + webfinger_discovery + when 'openid-configuration' + openid_configuration + else + raise HttpError::NotFound + end + end + + private + + def webfinger_discovery + jrd = { + links: [{ + rel: OpenIDConnect::Discovery::Provider::Issuer::REL_VALUE, + href: "http://0.0.0.0:3000" + }] + } + jrd[:subject] = params[:resource] if params[:resource].present? + render json: jrd, content_type: "application/jrd+json" + end + + def openid_configuration + config = OpenIDConnect::Discovery::Provider::Config::Response.new( + issuer: "http://0.0.0.0:3000", + authorization_endpoint: "#{authorizations_url}/new", + token_endpoint: access_tokens_url, + userinfo_endpoint: user_info_url, + jwks_uri: "#{authorizations_url}/jwks.json", + registration_endpoint: "http://0.0.0.0:3000/connect", + scopes_supported: "iss", + response_types_supported: "Client.available_response_types", + grant_types_supported: "Client.available_grant_types", + request_object_signing_alg_values_supported: [:HS256, :HS384, :HS512], + 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'] + ) + render json: config + end +end diff --git a/config/routes.rb b/config/routes.rb index 4babaf6301c8c4204da91c3e96c393b104309150..147212a985da847f3204af159d5ff06fd1e0721f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -240,4 +240,13 @@ Diaspora::Application.routes.draw do # Startpage root :to => 'home#show' + + #OpenID Connect & OAuth + resource :openid do + resources :authorizations, only: [:new, :create] + match 'connect', to: 'connect#show', via: [:get, :post] + match '.well-known/:id', to: 'discovery#show' , :via => [:get, :post] + match 'user_info', to: 'user#show', :via => [:get, :post] + post 'access_tokens', to: proc { |env| TokenEndpoint.new.call(env) } + end end diff --git a/db/schema.rb b/db/schema.rb index 6a526fe46ece8d14da4451d32485bbd2b1fb0dec..59f51d5d1d4a87fa5dd26d48419f0919a51b5852 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -419,7 +419,7 @@ ActiveRecord::Schema.define(version: 20151003142048) do t.string "location", limit: 255 t.string "full_name", limit: 70 t.boolean "nsfw", default: false - t.boolean "public_details", default: false + t.boolean "public_details", default: false end add_index "profiles", ["full_name", "searchable"], name: "index_profiles_on_full_name_and_searchable", using: :btree diff --git a/lib/openid_connect/authorization_endpoint.rb b/lib/openid_connect/authorization_endpoint.rb new file mode 100644 index 0000000000000000000000000000000000000000..0d8b021c3997051f7d81fe9cdaaf2bcd553bc9bc --- /dev/null +++ b/lib/openid_connect/authorization_endpoint.rb @@ -0,0 +1,82 @@ +class AuthorizationEndpoint + attr_accessor :app, :account, :client, :redirect_uri, :response_type, :scopes, :_request_, :request_uri, :request_object + delegate :call, to: :app + + def initialize(allow_approval = false, approved = false) + @account = nil + @app = Rack::OAuth2::Server::Authorize.new do |req, res| + @client = nil # Find the client + 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 + @scopes = req.scope.inject([]) do |_scopes_, scope| + _scopes_ << Scope.find_by_name(scope) or req.invalid_scope! "Unknown scope: #{scope}" + 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 + if Client.available_response_types.include? Array(req.response_type).collect(&:to_s).join(' ') + if allow_approval + if approved + approved! req, res + else + req.access_denied! + end + else + @response_type = req.response_type + end + else + req.unsupported_response_type! + end + end + end + + def approved!(req, res) + response_types = Array(req.response_type) + if response_types.include? :code + authorization = account.authorizations.create!(client: @client, redirect_uri: res.redirect_uri, nonce: req.nonce) + authorization.scopes << scopes + if @request_object + authorization.create_authorization_request_object!( + request_object: RequestObject.new( + jwt_string: @request_object.to_jwt(@client.secret, :HS256) + ) + ) + end + res.code = authorization.code + end + if response_types.include? :token + access_token = account.access_tokens.create!(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) + ) + ) + end + res.access_token = access_token.to_bearer_token + end + if response_types.include? :id_token + _id_token_ = account.id_tokens.create!( + 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) + ) + ) + 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 +end \ No newline at end of file diff --git a/lib/openid_connect/token_endpoint.rb b/lib/openid_connect/token_endpoint.rb new file mode 100644 index 0000000000000000000000000000000000000000..c2198497229c4fc903cc64382b3010c16307aee8 --- /dev/null +++ b/lib/openid_connect/token_endpoint.rb @@ -0,0 +1,29 @@ +class TokenEndpoint + attr_accessor :app + delegate :call, to: :app + + def initialize + @app = Rack::OAuth2::Server::Token.new do |req, res| + client = Client.find_by_identifier(req.client_id) || req.invalid_client! + client.secret == req.client_secret || req.invalid_client! + case req.grant_type + when :client_credentials + res.access_token = client.access_tokens.create!.to_bearer_token + when :authorization_code + authorization = client.authorizations.valid.find_by_code(req.code) + req.invalid_grant! if authorization.blank? || !authorization.valid_redirect_uri?(req.redirect_uri) + access_token = authorization.access_token + res.access_token = access_token.to_bearer_token + if access_token.accessible?(Scope::OPENID) + res.id_token = access_token.account.id_tokens.create!( + client: access_token.client, + nonce: authorization.nonce, + request_object: authorization.request_object + ).to_response_object.to_jwt IdToken.config[:private_key] + end + else + req.unsupported_grant_type! + end + end + end +end \ No newline at end of file