Skip to content

Commit 5e82086

Browse files
authored
Authorization Flow Overhaul
* Added a list of authorization tasks to the user's session This allows to dynamically add tasks the user must perform before an authorization_code is issued * Replaced sucessive [] operators by dig() * Accept `prompt` and `max_age` parameters * Include `auth_time` in id_token * Adjusted a few error codes * Support for plain OAuth Authorization Flows * Better Requested Claims Handling * Remove dead code * More intuitive code for scope granting * Allow to request claims for the userinfo endpoint This introduces a new claim `omejdn_reserved` in the access token. It stores information for later retrieval, e.g. requested claims to be returned from `/userinfo` * Fixed a bug regarding the redirect_uri * Fix Tests * Fix spec reference * Only include the omejdn_reserved key when it contains actual data * Fixed switched expected/actual values in OAuth2 tests * Added tests for `/userinfo` endpoint
1 parent 24b9e8a commit 5e82086

14 files changed

+408
-356
lines changed

lib/config.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@ def self.base_config
3838

3939
def self.base_config=(config)
4040
# Make sure those are integers
41-
config['token']['expiration'] = config['token']['expiration'].to_i
42-
if config['id_token'] && config['id_token']['expiration']
43-
config['id_token']['expiration'] =
44-
config['id_token']['expiration'].to_i
41+
%w[token id_token].map { |t| config[t] }.compact.each do |c|
42+
c['expiration'] = c['expiration'].to_i
4543
end
4644
write_config OMEJDN_BASE_CONFIG_FILE, config.to_yaml
4745
end

lib/db_plugins/user_db_plugin_ldap.rb

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def map_oidc_claim(user, key, value)
6868
def ldap_entry_to_user(entry)
6969
user_backend_config = Config.user_backend_config
7070
user = User.new
71-
user.username = entry[user_backend_config['ldap']['uidKey']][0]
71+
user.username = entry.dig(user_backend_config.dig('ldap', 'uidKey'), 0)
7272
user.extern = true
7373
user.password = nil
7474
user.backend = 'ldap'
@@ -83,9 +83,9 @@ def ldap_entry_to_user(entry)
8383
def load_users
8484
user_backend_config = Config.user_backend_config
8585
ldap = Net::LDAP.new
86-
ldap.host = user_backend_config['ldap']['host']
87-
ldap.port = user_backend_config['ldap']['port']
88-
base_dn = user_backend_config['ldap']['baseDN']
86+
ldap.host = user_backend_config.dig('ldap', 'host')
87+
ldap.port = user_backend_config.dig('ldap', 'port')
88+
base_dn = user_backend_config.dig('ldap', 'baseDN')
8989
t_users = []
9090
ldap.search(base: base_dn) do |entry|
9191
puts "DN: #{entry.dn}"
@@ -110,12 +110,12 @@ def users
110110

111111
def bind(config, bdn, password)
112112
ldap = Net::LDAP.new
113-
ldap.host = config['ldap']['host']
114-
ldap.port = config['ldap']['port']
113+
ldap.host = config.dig('ldap', 'host')
114+
ldap.port = config.dig('ldap', 'port')
115115
puts "Trying bind for #{bdn}"
116116
ldap = Net::LDAP.new({
117-
host: config['ldap']['host'],
118-
port: config['ldap']['port'],
117+
host: config.dig('ldap', 'host'),
118+
port: config.dig('ldap', 'port'),
119119
auth: {
120120
method: :simple,
121121
username: bdn,
@@ -133,9 +133,9 @@ def bind(config, bdn, password)
133133

134134
def nobind(config)
135135
Net::LDAP.new({
136-
host: config['ldap']['host'],
137-
port: config['ldap']['port'],
138-
base: config['ldap']['base_dn'],
136+
host: config.dig('ldap', 'host'),
137+
port: config.dig('ldap', 'port'),
138+
base: config.dig('ldap', 'baseDN'),
139139
verbose: true,
140140
encryption: {
141141
method: :simple_tls,
@@ -147,8 +147,8 @@ def nobind(config)
147147
def lookup_user(user, config)
148148
return @dn_cache[user.username] unless @dnCache[user.username].nil?
149149

150-
connect(config).search(base: config['ldap']['baseDN'],
151-
filter: Net::LDAP::Filter.eq(config['ldap']['uidKey'],
150+
connect(config).search(base: config.dig('ldap', 'baseDN'),
151+
filter: Net::LDAP::Filter.eq(config.dig('ldap', 'uidKey'),
152152
user.username)) do |entry|
153153
return entry.dn
154154
end
@@ -164,8 +164,8 @@ def verify_credential(user, password)
164164

165165
puts "Trying bind for #{user_dn}"
166166
Net::LDAP.new({
167-
host: user_backend_config['ldap']['host'],
168-
port: user_backend_config['ldap']['port'],
167+
host: user_backend_config.dig('ldap', 'host'),
168+
port: user_backend_config.dig('ldap', 'port'),
169169
auth: {
170170
method: :simple,
171171
username: user_dn,
@@ -190,8 +190,8 @@ def find_by_id(username)
190190
user_backend_config = Config.user_backend_config
191191
ldap = connect(user_backend_config)
192192
p ldap
193-
base_dn = user_backend_config['ldap']['baseDN']
194-
uid_key = user_backend_config['ldap']['uidKey']
193+
base_dn = user_backend_config.dig('ldap', 'baseDN')
194+
uid_key = user_backend_config.dig('ldap', 'uidKey')
195195
puts "Looking for #{uid_key}=#{username}"
196196
filter = Net::LDAP::Filter.eq(uid_key, username)
197197
ldap.search(verbose: true, base: base_dn, filter: filter) do |entry|

lib/db_plugins/user_db_plugin_sqlite.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class SqliteUserDb < UserDb
77
def create_user(user)
88
user_backend_config = Config.user_backend_config
9-
db = SQLite3::Database.open user_backend_config['sqlite']['location']
9+
db = SQLite3::Database.open user_backend_config.dig('sqlite', 'location')
1010
db.execute 'CREATE TABLE IF NOT EXISTS password(username TEXT PRIMARY KEY, password TEXT)'
1111
db.execute 'CREATE TABLE IF NOT EXISTS attributes(username TEXT, key TEXT, value TEXT, PRIMARY KEY (username, key))'
1212
db.execute 'INSERT INTO password(username, password) VALUES(?, ?)', user.username, user.password
@@ -19,8 +19,8 @@ def create_user(user)
1919

2020
def delete_user(username)
2121
user_backend_config = Config.user_backend_config
22-
db = SQLite3::Database.open user_backend_config['sqlite']['location']
23-
user_in_sqlite = (db.execute 'SELECT EXISTS(SELECT 1 FROM password WHERE username=?)', username)[0][0]
22+
db = SQLite3::Database.open user_backend_config.dig('sqlite', 'location')
23+
user_in_sqlite = (db.execute 'SELECT EXISTS(SELECT 1 FROM password WHERE username=?)', username).dig(0, 0)
2424
return false unless user_in_sqlite == 1
2525

2626
db.execute 'DELETE FROM password WHERE username=?', username
@@ -43,8 +43,8 @@ def delete_missing_attributes(user, db)
4343

4444
def update_user(user)
4545
user_backend_config = Config.user_backend_config
46-
db = SQLite3::Database.open user_backend_config['sqlite']['location']
47-
user_in_sqlite = (db.execute 'SELECT EXISTS(SELECT 1 FROM password WHERE username=?)', user.username)[0][0]
46+
db = SQLite3::Database.open user_backend_config.dig('sqlite', 'location')
47+
user_in_sqlite = (db.execute 'SELECT EXISTS(SELECT 1 FROM password WHERE username=?)', user.username).dig(0, 0)
4848
return false unless user_in_sqlite == 1
4949

5050
db.results_as_hash = true
@@ -63,7 +63,7 @@ def verify_credential(user, password)
6363

6464
def load_users
6565
user_backend_config = Config.user_backend_config
66-
db = SQLite3::Database.open user_backend_config['sqlite']['location']
66+
db = SQLite3::Database.open user_backend_config.dig('sqlite', 'location')
6767
db.results_as_hash = true
6868
begin
6969
t_users = db.execute 'SELECT * FROM password'
@@ -94,8 +94,8 @@ def users
9494

9595
def change_password(user, password)
9696
user_backend_config = Config.user_backend_config
97-
db = SQLite3::Database.open user_backend_config['sqlite']['location']
98-
user_in_sqlite = (db.execute 'SELECT EXISTS(SELECT 1 FROM password WHERE username=?)', user.username)[0][0]
97+
db = SQLite3::Database.open user_backend_config.dig('sqlite', 'location')
98+
user_in_sqlite = (db.execute 'SELECT EXISTS(SELECT 1 FROM password WHERE username=?)', user.username).dig(0, 0)
9999
return false unless user_in_sqlite == 1
100100

101101
db.execute 'UPDATE password SET password=? WHERE username=?', password, user.username

lib/db_plugins/user_db_plugin_yaml.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def update_user(user)
3939

4040
def load_users
4141
user_backend_config = Config.user_backend_config
42-
(YAML.safe_load File.read user_backend_config['yaml']['location']) || []
42+
(YAML.safe_load File.read user_backend_config.dig('yaml', 'location')) || []
4343
end
4444

4545
def write_user_db(users)
@@ -48,7 +48,7 @@ def write_user_db(users)
4848
users_yaml << user.to_dict
4949
end
5050
user_backend_config = Config.user_backend_config
51-
Config.write_config(user_backend_config['yaml']['location'], users_yaml.to_yaml)
51+
Config.write_config(user_backend_config.dig('yaml', 'location'), users_yaml.to_yaml)
5252
end
5353

5454
def users

lib/oauth_helper.rb

Lines changed: 22 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -24,47 +24,26 @@ def self.token_response(access_token, scopes, id_token)
2424
response = {}
2525
response['access_token'] = access_token
2626
response['id_token'] = id_token unless id_token.nil?
27-
response['expires_in'] = Config.base_config['token']['expiration']
27+
response['expires_in'] = Config.base_config.dig('token', 'expiration')
2828
response['token_type'] = 'bearer'
2929
response['scope'] = scopes.join ' '
3030
JSON.generate response
3131
end
3232

33-
def self.supported_scopes
34-
scopes = Set[]
35-
Config.client_config.each do |_client_id, arr|
36-
next if arr['scopes'].nil?
37-
38-
arr['scopes'].each do |scope|
39-
scopes.add(scope)
40-
end
41-
end
42-
scopes.to_a
43-
end
44-
45-
def self.userinfo(user, token)
46-
userinfo = {}
33+
def self.userinfo(client, user, token)
34+
req_claims = token.dig('omejdn_reserved', 'userinfo_req_claims')
35+
userinfo = TokenHelper.map_claims_to_userinfo(user.attributes, req_claims, client, token['scope'].split)
4736
userinfo['sub'] = user.username
48-
user.attributes.each do |attribute|
49-
token[0].each do |key, _|
50-
next unless attribute['key'] == key
51-
52-
TokenHelper.add_jwt_claim(userinfo, key, attribute['value'])
53-
end
54-
end
5537
userinfo
5638
end
5739

58-
def self.default_scopes
59-
scopes = []
60-
Config.scope_mapping_config.each do |mapping|
61-
scopes << mapping[0]
62-
end
63-
scopes
40+
def self.supported_scopes
41+
Config.scope_mapping_config.map { |m| m[0] }
6442
end
6543

6644
def self.error_response(error, desc = '')
6745
response = { 'error' => error, 'error_description' => desc }
46+
p response
6847
JSON.generate response
6948
end
7049

@@ -106,40 +85,33 @@ def self.generate_jwks
10685
def self.openid_configuration(host, path)
10786
base_config = Config.base_config
10887
metadata = {}
109-
metadata['issuer'] = base_config['token']['issuer']
88+
metadata['issuer'] = base_config.dig('token', 'issuer')
11089
metadata['authorization_endpoint'] = "#{path}/authorize"
11190
metadata['token_endpoint'] = "#{path}/token"
11291
metadata['userinfo_endpoint'] = "#{path}/userinfo"
11392
metadata['jwks_uri'] = "#{host}/.well-known/jwks.json"
11493
# metadata["registration_endpoint"] = "#{host}/FIXME"
115-
metadata['scopes_supported'] = OAuthHelper.default_scopes
94+
metadata['scopes_supported'] = OAuthHelper.supported_scopes
11695
metadata['response_types_supported'] = ['code']
11796
metadata['response_modes_supported'] = ['query'] # FIXME: we only do query atm no fragment
11897
metadata['grant_types_supported'] = ['authorization_code']
119-
metadata['id_token_signing_alg_values_supported'] = base_config['token']['algorithm']
98+
metadata['id_token_signing_alg_values_supported'] = base_config.dig('token', 'algorithm')
12099
metadata
121100
end
122101

123-
def self.verify_authorization_request(params)
124-
client = Client.find_by_id params['client_id']
125-
unless params[:response_type] == 'code'
126-
raise OAuthHelper.error_response 'unsupported_response_type',
127-
"Given: #{params[:response_type]}"
102+
def self.adapt_requested_claims(req_claims)
103+
# https://tools.ietf.org/id/draft-spencer-oauth-claims-00.html#rfc.section.3
104+
known_sinks = %w[access_token id_token userinfo]
105+
default_sinks = ['access_token']
106+
known_sinks.each do |sink|
107+
req_claims[sink] ||= {}
108+
req_claims[sink].merge!(req_claims['*'] || {})
128109
end
129-
raise OAuthHelper.error_response 'invalid_client', '' if client.nil?
130-
unless client.redirect_uri ==
131-
CGI.unescape(params[:redirect_uri].gsub('%20', '+'))
132-
raise OAuthHelper.error_response 'invalid_redirect_uri', ''
110+
default_sinks.each do |sink|
111+
req_claims[sink].merge!(req_claims['?'] || {})
133112
end
134-
end
135-
136-
def self.handle_authorization_request(params)
137-
verify_authorization_request(params)
138-
139-
# Seems to be in order
140-
haml :authorization_page, locals: {
141-
client: client,
142-
scopes: params[:scope]
143-
}
113+
req_claims.delete('*')
114+
req_claims.delete('?')
115+
req_claims
144116
end
145117
end

lib/server.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ def self.load_pkey(token_type = 'token')
5555
end
5656

5757
def self.load_skey(token_type = 'token')
58-
filename = Config.base_config[token_type]['signing_key']
58+
filename = Config.base_config.dig(token_type, 'signing_key')
5959
setup_skey(filename) unless File.exist? filename
6060
sk = OpenSSL::PKey::RSA.new File.read(filename)
61-
pk = load_pkey(token_type).select { |c| c.dig('certs', 0) && (c['certs'][0].check_private_key sk) }.first
61+
pk = load_pkey(token_type).select { |c| c.dig('certs', 0) && (c.dig('certs', 0).check_private_key sk) }.first
6262
(pk || {}).merge({ 'sk' => sk, 'pk' => sk.public_key })
6363
end
6464
end

lib/token_helper.rb

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,27 @@ class OAuth2Error < RuntimeError
1515

1616
# A helper for building JWT access tokens and ID tokens
1717
class TokenHelper
18-
def server_key; end
19-
2018
def self.build_access_token_stub(attrs, client, scopes, resources, claims)
2119
base_config = Config.base_config
2220
now = Time.new.to_i
23-
{
21+
token = {
2422
'scope' => (scopes.join ' '),
2523
'aud' => resources,
26-
'iss' => base_config['token']['issuer'],
24+
'iss' => base_config.dig('token', 'issuer'),
2725
'nbf' => now,
2826
'iat' => now,
2927
'jti' => Base64.urlsafe_encode64(rand(2**64).to_s),
30-
'exp' => now + base_config['token']['expiration'],
28+
'exp' => now + base_config.dig('token', 'expiration'),
3129
'client_id' => client.client_id
32-
}.merge(map_claims_to_userinfo(attrs, claims, client, scopes))
30+
}
31+
reserved = {}
32+
reserved['userinfo_req_claims'] = claims['userinfo'] unless (claims['userinfo'] || {}).empty?
33+
token['omejdn_reserved'] = reserved unless reserved.empty?
34+
token.merge(map_claims_to_userinfo(attrs, claims['access_token'], client, scopes))
3335
end
3436

3537
# Builds a JWT access token for client including scopes and attributes
36-
def self.build_access_token(client, scopes, resources, user, claims)
38+
def self.build_access_token(client, user, scopes, claims, resources)
3739
# Use user attributes if we have a user context, else use client
3840
# attributes.
3941
if user
@@ -55,7 +57,7 @@ def self.address_claim?(key)
5557
def self.add_jwt_claim(jwt_body, key, value)
5658
# Address is handled differently. For reasons...
5759
if address_claim?(key)
58-
jwt_body['address'] = {} if jwt_body['address'].nil?
60+
jwt_body['address'] ||= {}
5961
jwt_body['address'][key] = value
6062
return
6163
end
@@ -64,6 +66,7 @@ def self.add_jwt_claim(jwt_body, key, value)
6466

6567
def self.map_claims_to_userinfo(attrs, claims, client, scopes)
6668
new_payload = {}
69+
claims ||= {}
6770

6871
# Add attribute if it was requested indirectly through OIDC
6972
# scope and scope is allowed for client.
@@ -75,12 +78,12 @@ def self.map_claims_to_userinfo(attrs, claims, client, scopes)
7578
# Add attribute if it was specifically requested through OIDC
7679
# claims parameter.
7780
attrs.each do |attr|
78-
next unless claims.key?(attr['key']) && !claims[attr['key']].nil?
81+
next unless (name = claims[attr['key']])
7982

80-
if attr['dynamic'] && claims[attr['key']]['value']
81-
add_jwt_claim(new_payload, attr['key'], claims[attr['key']]['value'])
82-
elsif attr['dynamic'] && claims[attr['key']]['values']
83-
add_jwt_claim(new_payload, attr['key'], claims[attr['key']]['values'][0])
83+
if attr['dynamic'] && name['value']
84+
add_jwt_claim(new_payload, attr['key'], name['value'])
85+
elsif attr['dynamic'] && name['values']
86+
add_jwt_claim(new_payload, attr['key'], name.dig('values', 0))
8487
elsif attr['value']
8588
add_jwt_claim(new_payload, attr['key'], attr['value'])
8689
end
@@ -89,30 +92,21 @@ def self.map_claims_to_userinfo(attrs, claims, client, scopes)
8992
end
9093

9194
# Builds a JWT ID token for client including user attributes
92-
def self.build_id_token(client, uentry, nonce, claims, scopes)
95+
def self.build_id_token(client, user, scopes, claims, nonce)
9396
base_config = Config.base_config
9497
now = Time.new.to_i
9598
new_payload = {
9699
'aud' => client.client_id,
97-
'iss' => base_config['token']['issuer'],
98-
'sub' => uentry.username,
100+
'iss' => base_config.dig('id_token', 'issuer'),
101+
'sub' => user.username,
99102
'nbf' => now,
100103
'iat' => now,
101-
'exp' => now + base_config['id_token']['expiration']
102-
}.merge(map_claims_to_userinfo(uentry.attributes, claims, client, scopes))
104+
'exp' => now + base_config.dig('id_token', 'expiration'),
105+
'auth_time' => user.auth_time
106+
}.merge(map_claims_to_userinfo(user.attributes, claims['id_token'], client, scopes))
103107
new_payload['nonce'] = nonce unless nonce.nil?
104-
signing_material = Server.load_skey('token')
108+
signing_material = Server.load_skey('id_token')
105109
kid = JSON::JWK.new(signing_material['pk'])[:kid]
106110
JWT.encode new_payload, signing_material['sk'], 'RS256', { typ: 'JWT', kid: kid }
107111
end
108-
109-
# TODO: old, might needs to be changed
110-
def self.subject_from_cert(cert)
111-
Encoding.default_external = Encoding::UTF_8
112-
113-
subject = cert.subject.to_s(OpenSSL::X509::Name::ONELINE & ~ASN1_STRFLGS_ESC_MSB).delete(' ')
114-
subject.force_encoding(Encoding::UTF_8)
115-
end
116-
117-
private :server_key
118112
end

0 commit comments

Comments
 (0)