Skip to content

Commit

Permalink
feature: Add webfinger and keys endpoints for discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
toupeira committed Oct 5, 2016
1 parent a16caa8 commit f70898b
Show file tree
Hide file tree
Showing 13 changed files with 221 additions and 76 deletions.
46 changes: 39 additions & 7 deletions app/controllers/doorkeeper/openid_connect/discovery_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,31 @@ module OpenidConnect
class DiscoveryController < ::Doorkeeper::ApplicationController
include Doorkeeper::Helpers::Controller

def show
render json: provider_configuration
WEBFINGER_RELATION = 'http://openid.net/specs/connect/1.0/issuer'

def provider
render json: provider_response
end

def webfinger
render json: webfinger_response
end

def keys
render json: keys_response
end

private

def provider_configuration
def provider_response
doorkeeper = ::Doorkeeper.configuration
openid_connect = ::Doorkeeper::OpenidConnect.configuration

{
issuer: openid_connect.issuer,
authorization_endpoint: oauth_authorization_url(protocol: :https),
token_endpoint: oauth_token_url(protocol: :https),
userinfo_endpoint: oauth_userinfo_url(protocol: :https),

# TODO: implement controller
#jwks_uri: oauth_keys_url(protocol: :https),
jwks_uri: oauth_discovery_keys_url(protocol: :https),

scopes_supported: doorkeeper.scopes,

Expand Down Expand Up @@ -48,6 +55,31 @@ def provider_configuration
],
}
end

def webfinger_response
{
subject: params.require(:resource),
links: [
{
rel: WEBFINGER_RELATION,
href: root_url(protocol: :https),
}
]
}
end

def keys_response
signing_key = Doorkeeper::OpenidConnect.signing_key

{
keys: [
signing_key.slice(:kty, :kid, :e, :n).merge(
use: 'sig',
alg: Doorkeeper::OpenidConnect::SIGNING_ALGORITHM
)
]
}
end
end
end
end
9 changes: 3 additions & 6 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ en:
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'

#configuration error messages
jwt_private_key_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.jwt_private_key missing configuration.'
jwt_public_key_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.jwt_public_key missing configuration.'
issuer: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.issuer missing configuration.'
resource_owner_from_access_token_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token missing configuration.'
subject_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.subject missing configuration.'
# Configuration error messages
resource_owner_from_access_token_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token missing configuration.'
subject_not_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.subject missing configuration.'
2 changes: 1 addition & 1 deletion doorkeeper-openid_connect.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']

spec.add_runtime_dependency 'doorkeeper', '~> 4.0'
spec.add_runtime_dependency 'sandal', '~> 0.6'
spec.add_runtime_dependency 'json-jwt', '~> 1.6.5'

spec.add_development_dependency 'rspec-rails'
spec.add_development_dependency 'factory_girl'
Expand Down
8 changes: 8 additions & 0 deletions lib/doorkeeper/openid_connect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,28 @@
require 'doorkeeper/openid_connect/rails/routes'

require 'doorkeeper'
require 'json/jwt'

module Doorkeeper
class << self
prepend OpenidConnect::DoorkeeperConfiguration
end

module OpenidConnect
# TODO: make this configurable
SIGNING_ALGORITHM = 'RS256'

def self.configured?
@config.present?
end

def self.installed?
configured?
end

def self.signing_key
JSON::JWK.new(OpenSSL::PKey.read(configuration.jws_private_key))
end
end
end

Expand Down
24 changes: 5 additions & 19 deletions lib/doorkeeper/openid_connect/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,33 +96,19 @@ def extended(base)

extend Option

option :jws_private_key,
default: (lambda do
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.jws_private_key_configured'))
nil
end)

option :jws_public_key,
default: (lambda do
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.jws_public_key_configured'))
nil
end)

option :issuer,
default: (lambda do
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.issuer_configured'))
nil
end)
option :jws_private_key
option :jws_public_key
option :issuer

option :resource_owner_from_access_token,
default: (lambda do
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.resource_owner_from_access_token_configured'))
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.resource_owner_from_access_token_not_configured'))
nil
end)

option :subject,
default: (lambda do
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.subject_configured'))
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.subject_not_configured'))
nil
end)

Expand Down
8 changes: 1 addition & 7 deletions lib/doorkeeper/openid_connect/models/id_token.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require 'sandal'

module Doorkeeper
module OpenidConnect
module Models
Expand All @@ -10,8 +8,6 @@ def initialize(access_token)
@access_token = access_token
@resource_owner = access_token.instance_eval(&Doorkeeper::OpenidConnect.configuration.resource_owner_from_access_token)
@issued_at = Time.now
@signer = Sandal::Sig::RS256.new(Doorkeeper::OpenidConnect.configuration.jws_private_key)
@public_key = Doorkeeper::OpenidConnect.configuration.jws_public_key
end

def claims
Expand All @@ -28,10 +24,8 @@ def as_json(options = {})
claims
end

# TODO make signature strategy configurable with keys?
# TODO move this out of the model
def as_jws_token
Sandal.encode_token(claims, @signer, typ: 'JWT')
JSON::JWT.new(claims).sign(Doorkeeper::OpenidConnect.signing_key).to_s
end

private
Expand Down
32 changes: 18 additions & 14 deletions lib/doorkeeper/openid_connect/rails/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,41 @@ def generate_routes!(options)
@mapping = Mapper.new.map(&@block)
routes.scope options[:scope] || 'oauth', as: 'oauth' do
map_route(:userinfo, :userinfo_routes)
map_route(:discovery, :discovery_routes)
end

routes.scope as: 'oauth' do
map_route(:discovery, :discovery_routes)
map_route(:discovery, :discovery_well_known_routes)
end
end

private

def map_route(name, method)
unless @mapping.skipped?(name)
send method, @mapping[name]
mapping = @mapping[name]

routes.scope controller: mapping[:controllers], as: mapping[:as] do
send method, mapping
end
end
end

def userinfo_routes(mapping)
routes.resource(
:userinfo,
path: 'userinfo',
only: [:show], as: mapping[:as],
controller: mapping[:controllers]
)
routes.get :show, path: 'userinfo', as: ''
end

def discovery_routes(mapping)
routes.resource(
:discovery,
path: '.well-known/openid-configuration',
only: [:show], as: mapping[:as],
controller: mapping[:controllers]
)
routes.scope path: 'discovery' do
routes.get :keys
end
end

def discovery_well_known_routes(mapping)
routes.scope path: '.well-known' do
routes.get :provider, path: 'openid-configuration'
routes.get :webfinger
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
require 'rails_helper'

describe Doorkeeper::OpenidConnect::DiscoveryController, type: :controller do
describe '#show' do
describe '#provider' do
it 'returns the provider configuration' do
get :show
configuration = JSON.parse(response.body)
get :provider
data = JSON.parse(response.body)

expect(configuration.sort).to eq({
expect(data.sort).to eq({
'issuer' => 'dummy',
'authorization_endpoint' => 'https://test.host/oauth/authorize',
'token_endpoint' => 'https://test.host/oauth/token',
'userinfo_endpoint' => 'https://test.host/oauth/userinfo',
'jwks_uri' => 'https://test.host/oauth/discovery/keys',

'scopes_supported' => ['openid'],

Expand All @@ -32,4 +33,45 @@
}.sort)
end
end

describe '#webfinger' do
it 'requires the resource parameter' do
expect do
get :webfinger
end.to raise_error ActionController::ParameterMissing
end

it 'returns the OpenID Connect relation' do
get :webfinger, resource: 'user@example.com'
data = JSON.parse(response.body)

expect(data.sort).to eq({
'subject' => 'user@example.com',
'links' => [
'rel' => 'http://openid.net/specs/connect/1.0/issuer',
'href' => 'https://test.host/',
],
}.sort)
end
end

describe '#keys' do
it 'returns the key parameters' do
get :keys
data = JSON.parse(response.body)

expect(data.sort).to eq({
'keys' => [
{
'kty' => 'RSA',
'kid' => 'IqYwZo2cE6hsyhs48cU8QHH4GanKIx0S4Dc99kgTIMA',
'e' => 'AQAB',
'n' => 'sjdnSA6UWUQQHf6BLIkIEUhMRNBJC1NN_pFt1EJmEiI88GS0ceROO5B5Ooo9Y3QOWJ_n-u1uwTHBz0HCTN4wgArWd1TcqB5GQzQRP4eYnWyPfi4CfeqAHzQp-v4VwbcK0LW4FqtW5D0dtrFtI281FDxLhARzkhU2y7fuYhL8fVw5rUhE8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2DGkDInU1BmF75tEtBJAVLzWG_j4LPZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy-ulOdUsMlB6yo8qKTY1-AbZ5jzneHbGDU_O8QjYvii1WDmJ60t0jXicmOkGrOhruOptw',
'use' => 'sig',
'alg' => 'RS256',
}
],
}.sort)
end
end
end
42 changes: 42 additions & 0 deletions spec/dummy/config/initializers/doorkeeper_openid_connect.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,48 @@
Doorkeeper::OpenidConnect.configure do
issuer 'dummy'

jws_private_key <<-EOL
-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEAsjdnSA6UWUQQHf6BLIkIEUhMRNBJC1NN/pFt1EJmEiI88GS0
ceROO5B5Ooo9Y3QOWJ/n+u1uwTHBz0HCTN4wgArWd1TcqB5GQzQRP4eYnWyPfi4C
feqAHzQp+v4VwbcK0LW4FqtW5D0dtrFtI281FDxLhARzkhU2y7fuYhL8fVw5rUhE
8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2DGkDInU1BmF75tEtBJAVLzWG/j4L
PZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy+ulOdUsMlB6yo8qKTY1+AbZ5jzneHb
GDU/O8QjYvii1WDmJ60t0jXicmOkGrOhruOptwIDAQABAoIBAQChYNwMeu9IugJi
NsEf4+JDTBWMRpOuRrwcpfIvQAUPrKNEB90COPvCoju0j9OxCDmpdPtq1K/zD6xx
khlw485FVAsKufSp4+g6GJ75yT6gZtq1JtKo1L06BFFzb7uh069eeP7+wB6JxPHw
KlAqwxvsfADhxeolQUKCTMb3Vjv/Aw2cO/nn6RAOeftw2aDmFy8Xl+oTUtSxyib0
YCdU9cK8MxsxDdmowwHp04xRTm/wfG5hLEn7HMz1PP86iP9BiFsCqTId9dxEUTS1
K+VAt9FbxRAq5JlBocxUMHNxLigb94Ca2FOMR7F6l/tronLfHD801YoObF0fN9qW
Cgw4aTO5AoGBAOR79hiZVM7/l1cBid7hKSeMWKUZ/nrwJsVfNpu1H9xt9uDu+79U
mcGfM7pm7L2qCNGg7eeWBHq2CVg/XQacRNtcTlomFrw4tDXUkFN1hE56t1iaTs9m
dN9IDr6jFgf6UaoOxxoPT9Q1ZtO46l043Nzrkoz8cBEBaBY20bUDwCYjAoGBAMet
tt1ImGF1cx153KbOfjl8v54VYUVkmRNZTa1E821nL/EMpoONSqJmRVsX7grLyPL1
QyZe245NOvn63YM0ng0rn2osoKsMVJwYBEYjHL61iF6dPtW5p8FIs7auRnC3NrG0
XxHATZ4xhHD0iIn14iXh0XIhUVk+nGktHU1gbmVdAoGBANniwKdqqS6RHKBTDkgm
Dhnxw6MGa+CO3VpA1xGboxuRHeoY3KfzpIC5MhojBsZDvQ8zWUwMio7+w2CNZEfm
g99wYiOjyPCLXocrAssj+Rzh97AdzuQHf5Jh4/W2Dk9jTbdPSl02ltj2Z+2lnJFz
pWNjnqimHrSI09rDQi5NulJjAoGBAImquujVpDmNQFCSNA7NTzlTSMk09FtjgCZW
67cKUsqa2fLXRfZs84gD+s1TMks/NMxNTH6n57e0h3TSAOb04AM0kDQjkKJdXfhA
lrHEg4z4m4yf3TJ9Tat09HJ+tRIBPzRFp0YVz23Btg4qifiUDdcQWdbWIb/l6vCY
qhsu4O4BAoGBANbceYSDYRdT7a5QjJGibkC90Z3vFe4rDTBgZWg7xG0cpSU4JNg7
SFR3PjWQyCg7aGGXiooCM38YQruACTj0IFub24MFRA4ZTXvrACvpsVokJlQiG0Z4
tuQKYki41JvYqPobcq/rLE/AM7PKJftW35nqFuj0MrsUwPacaVwKBf5J
-----END RSA PRIVATE KEY-----
EOL

jws_public_key <<-EOL
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjdnSA6UWUQQHf6BLIkI
EUhMRNBJC1NN/pFt1EJmEiI88GS0ceROO5B5Ooo9Y3QOWJ/n+u1uwTHBz0HCTN4w
gArWd1TcqB5GQzQRP4eYnWyPfi4CfeqAHzQp+v4VwbcK0LW4FqtW5D0dtrFtI281
FDxLhARzkhU2y7fuYhL8fVw5rUhE8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2
DGkDInU1BmF75tEtBJAVLzWG/j4LPZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy+u
lOdUsMlB6yo8qKTY1+AbZ5jzneHbGDU/O8QjYvii1WDmJ60t0jXicmOkGrOhruOp
twIDAQAB
-----END PUBLIC KEY-----
EOL

resource_owner_from_access_token do |access_token|
User.find_by(id: access_token.resource_owner_id)
end
Expand Down
2 changes: 2 additions & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Rails.application.routes.draw do
use_doorkeeper
use_doorkeeper_openid_connect

root 'dummy#index'
end
Loading

0 comments on commit f70898b

Please sign in to comment.