Skip to content

Commit f70898b

Browse files
committed
feature: Add webfinger and keys endpoints for discovery
1 parent a16caa8 commit f70898b

File tree

13 files changed

+221
-76
lines changed

13 files changed

+221
-76
lines changed

app/controllers/doorkeeper/openid_connect/discovery_controller.rb

+39-7
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,31 @@ module OpenidConnect
33
class DiscoveryController < ::Doorkeeper::ApplicationController
44
include Doorkeeper::Helpers::Controller
55

6-
def show
7-
render json: provider_configuration
6+
WEBFINGER_RELATION = 'http://openid.net/specs/connect/1.0/issuer'
7+
8+
def provider
9+
render json: provider_response
10+
end
11+
12+
def webfinger
13+
render json: webfinger_response
14+
end
15+
16+
def keys
17+
render json: keys_response
818
end
919

1020
private
1121

12-
def provider_configuration
22+
def provider_response
1323
doorkeeper = ::Doorkeeper.configuration
1424
openid_connect = ::Doorkeeper::OpenidConnect.configuration
15-
1625
{
1726
issuer: openid_connect.issuer,
1827
authorization_endpoint: oauth_authorization_url(protocol: :https),
1928
token_endpoint: oauth_token_url(protocol: :https),
2029
userinfo_endpoint: oauth_userinfo_url(protocol: :https),
21-
22-
# TODO: implement controller
23-
#jwks_uri: oauth_keys_url(protocol: :https),
30+
jwks_uri: oauth_discovery_keys_url(protocol: :https),
2431

2532
scopes_supported: doorkeeper.scopes,
2633

@@ -48,6 +55,31 @@ def provider_configuration
4855
],
4956
}
5057
end
58+
59+
def webfinger_response
60+
{
61+
subject: params.require(:resource),
62+
links: [
63+
{
64+
rel: WEBFINGER_RELATION,
65+
href: root_url(protocol: :https),
66+
}
67+
]
68+
}
69+
end
70+
71+
def keys_response
72+
signing_key = Doorkeeper::OpenidConnect.signing_key
73+
74+
{
75+
keys: [
76+
signing_key.slice(:kty, :kid, :e, :n).merge(
77+
use: 'sig',
78+
alg: Doorkeeper::OpenidConnect::SIGNING_ALGORITHM
79+
)
80+
]
81+
}
82+
end
5183
end
5284
end
5385
end

config/locales/en.yml

+3-6
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ en:
1414
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
1515
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
1616

17-
#configuration error messages
18-
jwt_private_key_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.jwt_private_key missing configuration.'
19-
jwt_public_key_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.jwt_public_key missing configuration.'
20-
issuer: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.issuer missing configuration.'
21-
resource_owner_from_access_token_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token missing configuration.'
22-
subject_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.subject missing configuration.'
17+
# Configuration error messages
18+
resource_owner_from_access_token_not_configured: 'Failure due to Doorkeeper::OpenidConnect.configure.resource_owner_from_access_token missing configuration.'
19+
subject_not_configured: 'ID Token generation failed due to Doorkeeper::OpenidConnect.configure.subject missing configuration.'

doorkeeper-openid_connect.gemspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
1919
spec.require_paths = ['lib']
2020

2121
spec.add_runtime_dependency 'doorkeeper', '~> 4.0'
22-
spec.add_runtime_dependency 'sandal', '~> 0.6'
22+
spec.add_runtime_dependency 'json-jwt', '~> 1.6.5'
2323

2424
spec.add_development_dependency 'rspec-rails'
2525
spec.add_development_dependency 'factory_girl'

lib/doorkeeper/openid_connect.rb

+8
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,28 @@
1212
require 'doorkeeper/openid_connect/rails/routes'
1313

1414
require 'doorkeeper'
15+
require 'json/jwt'
1516

1617
module Doorkeeper
1718
class << self
1819
prepend OpenidConnect::DoorkeeperConfiguration
1920
end
2021

2122
module OpenidConnect
23+
# TODO: make this configurable
24+
SIGNING_ALGORITHM = 'RS256'
25+
2226
def self.configured?
2327
@config.present?
2428
end
2529

2630
def self.installed?
2731
configured?
2832
end
33+
34+
def self.signing_key
35+
JSON::JWK.new(OpenSSL::PKey.read(configuration.jws_private_key))
36+
end
2937
end
3038
end
3139

lib/doorkeeper/openid_connect/config.rb

+5-19
Original file line numberDiff line numberDiff line change
@@ -96,33 +96,19 @@ def extended(base)
9696

9797
extend Option
9898

99-
option :jws_private_key,
100-
default: (lambda do
101-
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.jws_private_key_configured'))
102-
nil
103-
end)
104-
105-
option :jws_public_key,
106-
default: (lambda do
107-
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.jws_public_key_configured'))
108-
nil
109-
end)
110-
111-
option :issuer,
112-
default: (lambda do
113-
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.issuer_configured'))
114-
nil
115-
end)
99+
option :jws_private_key
100+
option :jws_public_key
101+
option :issuer
116102

117103
option :resource_owner_from_access_token,
118104
default: (lambda do
119-
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.resource_owner_from_access_token_configured'))
105+
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.resource_owner_from_access_token_not_configured'))
120106
nil
121107
end)
122108

123109
option :subject,
124110
default: (lambda do
125-
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.subject_configured'))
111+
logger.warn(I18n.translate('doorkeeper.openid_connect.errors.messages.subject_not_configured'))
126112
nil
127113
end)
128114

lib/doorkeeper/openid_connect/models/id_token.rb

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require 'sandal'
2-
31
module Doorkeeper
42
module OpenidConnect
53
module Models
@@ -10,8 +8,6 @@ def initialize(access_token)
108
@access_token = access_token
119
@resource_owner = access_token.instance_eval(&Doorkeeper::OpenidConnect.configuration.resource_owner_from_access_token)
1210
@issued_at = Time.now
13-
@signer = Sandal::Sig::RS256.new(Doorkeeper::OpenidConnect.configuration.jws_private_key)
14-
@public_key = Doorkeeper::OpenidConnect.configuration.jws_public_key
1511
end
1612

1713
def claims
@@ -28,10 +24,8 @@ def as_json(options = {})
2824
claims
2925
end
3026

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

3731
private

lib/doorkeeper/openid_connect/rails/routes.rb

+18-14
Original file line numberDiff line numberDiff line change
@@ -25,37 +25,41 @@ def generate_routes!(options)
2525
@mapping = Mapper.new.map(&@block)
2626
routes.scope options[:scope] || 'oauth', as: 'oauth' do
2727
map_route(:userinfo, :userinfo_routes)
28+
map_route(:discovery, :discovery_routes)
2829
end
2930

3031
routes.scope as: 'oauth' do
31-
map_route(:discovery, :discovery_routes)
32+
map_route(:discovery, :discovery_well_known_routes)
3233
end
3334
end
3435

3536
private
3637

3738
def map_route(name, method)
3839
unless @mapping.skipped?(name)
39-
send method, @mapping[name]
40+
mapping = @mapping[name]
41+
42+
routes.scope controller: mapping[:controllers], as: mapping[:as] do
43+
send method, mapping
44+
end
4045
end
4146
end
4247

4348
def userinfo_routes(mapping)
44-
routes.resource(
45-
:userinfo,
46-
path: 'userinfo',
47-
only: [:show], as: mapping[:as],
48-
controller: mapping[:controllers]
49-
)
49+
routes.get :show, path: 'userinfo', as: ''
5050
end
5151

5252
def discovery_routes(mapping)
53-
routes.resource(
54-
:discovery,
55-
path: '.well-known/openid-configuration',
56-
only: [:show], as: mapping[:as],
57-
controller: mapping[:controllers]
58-
)
53+
routes.scope path: 'discovery' do
54+
routes.get :keys
55+
end
56+
end
57+
58+
def discovery_well_known_routes(mapping)
59+
routes.scope path: '.well-known' do
60+
routes.get :provider, path: 'openid-configuration'
61+
routes.get :webfinger
62+
end
5963
end
6064
end
6165
end

spec/controllers/doorkeeper/openid_connect/discovery_controller_spec.rb

+46-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
require 'rails_helper'
22

33
describe Doorkeeper::OpenidConnect::DiscoveryController, type: :controller do
4-
describe '#show' do
4+
describe '#provider' do
55
it 'returns the provider configuration' do
6-
get :show
7-
configuration = JSON.parse(response.body)
6+
get :provider
7+
data = JSON.parse(response.body)
88

9-
expect(configuration.sort).to eq({
9+
expect(data.sort).to eq({
1010
'issuer' => 'dummy',
1111
'authorization_endpoint' => 'https://test.host/oauth/authorize',
1212
'token_endpoint' => 'https://test.host/oauth/token',
1313
'userinfo_endpoint' => 'https://test.host/oauth/userinfo',
14+
'jwks_uri' => 'https://test.host/oauth/discovery/keys',
1415

1516
'scopes_supported' => ['openid'],
1617

@@ -32,4 +33,45 @@
3233
}.sort)
3334
end
3435
end
36+
37+
describe '#webfinger' do
38+
it 'requires the resource parameter' do
39+
expect do
40+
get :webfinger
41+
end.to raise_error ActionController::ParameterMissing
42+
end
43+
44+
it 'returns the OpenID Connect relation' do
45+
get :webfinger, resource: '[email protected]'
46+
data = JSON.parse(response.body)
47+
48+
expect(data.sort).to eq({
49+
'subject' => '[email protected]',
50+
'links' => [
51+
'rel' => 'http://openid.net/specs/connect/1.0/issuer',
52+
'href' => 'https://test.host/',
53+
],
54+
}.sort)
55+
end
56+
end
57+
58+
describe '#keys' do
59+
it 'returns the key parameters' do
60+
get :keys
61+
data = JSON.parse(response.body)
62+
63+
expect(data.sort).to eq({
64+
'keys' => [
65+
{
66+
'kty' => 'RSA',
67+
'kid' => 'IqYwZo2cE6hsyhs48cU8QHH4GanKIx0S4Dc99kgTIMA',
68+
'e' => 'AQAB',
69+
'n' => 'sjdnSA6UWUQQHf6BLIkIEUhMRNBJC1NN_pFt1EJmEiI88GS0ceROO5B5Ooo9Y3QOWJ_n-u1uwTHBz0HCTN4wgArWd1TcqB5GQzQRP4eYnWyPfi4CfeqAHzQp-v4VwbcK0LW4FqtW5D0dtrFtI281FDxLhARzkhU2y7fuYhL8fVw5rUhE8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2DGkDInU1BmF75tEtBJAVLzWG_j4LPZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy-ulOdUsMlB6yo8qKTY1-AbZ5jzneHbGDU_O8QjYvii1WDmJ60t0jXicmOkGrOhruOptw',
70+
'use' => 'sig',
71+
'alg' => 'RS256',
72+
}
73+
],
74+
}.sort)
75+
end
76+
end
3577
end

spec/dummy/config/initializers/doorkeeper_openid_connect.rb

+42
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,48 @@
11
Doorkeeper::OpenidConnect.configure do
22
issuer 'dummy'
33

4+
jws_private_key <<-EOL
5+
-----BEGIN RSA PRIVATE KEY-----
6+
MIIEpgIBAAKCAQEAsjdnSA6UWUQQHf6BLIkIEUhMRNBJC1NN/pFt1EJmEiI88GS0
7+
ceROO5B5Ooo9Y3QOWJ/n+u1uwTHBz0HCTN4wgArWd1TcqB5GQzQRP4eYnWyPfi4C
8+
feqAHzQp+v4VwbcK0LW4FqtW5D0dtrFtI281FDxLhARzkhU2y7fuYhL8fVw5rUhE
9+
8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2DGkDInU1BmF75tEtBJAVLzWG/j4L
10+
PZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy+ulOdUsMlB6yo8qKTY1+AbZ5jzneHb
11+
GDU/O8QjYvii1WDmJ60t0jXicmOkGrOhruOptwIDAQABAoIBAQChYNwMeu9IugJi
12+
NsEf4+JDTBWMRpOuRrwcpfIvQAUPrKNEB90COPvCoju0j9OxCDmpdPtq1K/zD6xx
13+
khlw485FVAsKufSp4+g6GJ75yT6gZtq1JtKo1L06BFFzb7uh069eeP7+wB6JxPHw
14+
KlAqwxvsfADhxeolQUKCTMb3Vjv/Aw2cO/nn6RAOeftw2aDmFy8Xl+oTUtSxyib0
15+
YCdU9cK8MxsxDdmowwHp04xRTm/wfG5hLEn7HMz1PP86iP9BiFsCqTId9dxEUTS1
16+
K+VAt9FbxRAq5JlBocxUMHNxLigb94Ca2FOMR7F6l/tronLfHD801YoObF0fN9qW
17+
Cgw4aTO5AoGBAOR79hiZVM7/l1cBid7hKSeMWKUZ/nrwJsVfNpu1H9xt9uDu+79U
18+
mcGfM7pm7L2qCNGg7eeWBHq2CVg/XQacRNtcTlomFrw4tDXUkFN1hE56t1iaTs9m
19+
dN9IDr6jFgf6UaoOxxoPT9Q1ZtO46l043Nzrkoz8cBEBaBY20bUDwCYjAoGBAMet
20+
tt1ImGF1cx153KbOfjl8v54VYUVkmRNZTa1E821nL/EMpoONSqJmRVsX7grLyPL1
21+
QyZe245NOvn63YM0ng0rn2osoKsMVJwYBEYjHL61iF6dPtW5p8FIs7auRnC3NrG0
22+
XxHATZ4xhHD0iIn14iXh0XIhUVk+nGktHU1gbmVdAoGBANniwKdqqS6RHKBTDkgm
23+
Dhnxw6MGa+CO3VpA1xGboxuRHeoY3KfzpIC5MhojBsZDvQ8zWUwMio7+w2CNZEfm
24+
g99wYiOjyPCLXocrAssj+Rzh97AdzuQHf5Jh4/W2Dk9jTbdPSl02ltj2Z+2lnJFz
25+
pWNjnqimHrSI09rDQi5NulJjAoGBAImquujVpDmNQFCSNA7NTzlTSMk09FtjgCZW
26+
67cKUsqa2fLXRfZs84gD+s1TMks/NMxNTH6n57e0h3TSAOb04AM0kDQjkKJdXfhA
27+
lrHEg4z4m4yf3TJ9Tat09HJ+tRIBPzRFp0YVz23Btg4qifiUDdcQWdbWIb/l6vCY
28+
qhsu4O4BAoGBANbceYSDYRdT7a5QjJGibkC90Z3vFe4rDTBgZWg7xG0cpSU4JNg7
29+
SFR3PjWQyCg7aGGXiooCM38YQruACTj0IFub24MFRA4ZTXvrACvpsVokJlQiG0Z4
30+
tuQKYki41JvYqPobcq/rLE/AM7PKJftW35nqFuj0MrsUwPacaVwKBf5J
31+
-----END RSA PRIVATE KEY-----
32+
EOL
33+
34+
jws_public_key <<-EOL
35+
-----BEGIN PUBLIC KEY-----
36+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjdnSA6UWUQQHf6BLIkI
37+
EUhMRNBJC1NN/pFt1EJmEiI88GS0ceROO5B5Ooo9Y3QOWJ/n+u1uwTHBz0HCTN4w
38+
gArWd1TcqB5GQzQRP4eYnWyPfi4CfeqAHzQp+v4VwbcK0LW4FqtW5D0dtrFtI281
39+
FDxLhARzkhU2y7fuYhL8fVw5rUhE8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2
40+
DGkDInU1BmF75tEtBJAVLzWG/j4LPZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy+u
41+
lOdUsMlB6yo8qKTY1+AbZ5jzneHbGDU/O8QjYvii1WDmJ60t0jXicmOkGrOhruOp
42+
twIDAQAB
43+
-----END PUBLIC KEY-----
44+
EOL
45+
446
resource_owner_from_access_token do |access_token|
547
User.find_by(id: access_token.resource_owner_id)
648
end

spec/dummy/config/routes.rb

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
Rails.application.routes.draw do
22
use_doorkeeper
33
use_doorkeeper_openid_connect
4+
5+
root 'dummy#index'
46
end

0 commit comments

Comments
 (0)