Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic claim values #23

Merged
merged 17 commits into from
Nov 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ Metrics/MethodLength:
Metrics/ClassLength:
Max: 200
Metrics/AbcSize:
Max: 25
Max: 50
Metrics/CyclomaticComplexity:
Max: 10
Max: 15
Metrics/PerceivedComplexity:
Max: 10
Max: 15
29 changes: 11 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ This server implements:

- [RFC7523 JWT bearer client authentication](https://tools.ietf.org/html/rfc7523#section-2.2) for [OAuth2](https://tools.ietf.org/html/rfc6749).
- [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html) with [discovery support](https://openid.net/specs/openid-connect-discovery-1_0.html) and [dynamic client registration](https://openid.net/specs/openid-connect-registration-1_0.html)
- [RFC8707 Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707) for [OAuth 2.0](https://tools.ietf.org/html/rfc6749)

Supported non-Standards:

- [draft-spencer-oauth-claims-00](https://tools.ietf.org/id/draft-spencer-oauth-claims-00.html#rfc.section.3)

**NOTE**: Omejdn only implements *two* grant types:

Expand Down Expand Up @@ -71,7 +76,7 @@ By default, omejdn uses the following directory structure for configurations and
\_ ...

You may use the default configurations from this repository as a starting
point and create your own setup accordingly.
point and create your own setup accordingly. Please refer to [the wiki](https://github.com/Fraunhofer-AISEC/omejdn-server/wiki/) for a description of available configuration options.
In order to start the omejdn service, you need to install the dependencies and
run it:

Expand Down Expand Up @@ -111,6 +116,10 @@ for testing, your can execute:

- APP_ENV: May be set to 'production' to prevent debug output such as logging sensitive information to stdout.
- HOST: May be set to modify the host config variable (useful for docker-compose deployments)
- OMEJDN_PATH_PREFIX: May be set to modify the path_prefix config variable
- BIND_TO: May be set to modify the bind_to config variable
- ALLOW_ORIGIN: May be set to modify the allow_origin config variable
- OMEJDN_ADMIN: May be set to username:password to create an admin user
- OMEJDN_JWT_AUD_OVERRIDE: May be set to modify the expected 'aud' claim in a JWT assertion in a client_credentials flow. The standard usually expects the claim to contain the host, hence use this only if necessary.

Setting the environment variables depends on how you run the service.
Expand All @@ -134,26 +143,10 @@ In order to generate your own key pair with a self-signed pulic key
for testing, your can execute:

$ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
$ cp cert.pem keys/<clientID>.cert

You may choose any valid filename for the certificate.
Then, you need to add your client ***clientID*** to the config file
`config/clients.yml`:

- clientID: <client_id>
name: My Client
redirect_uri: <uri> (optional, required for OIDC)
allowed_scopes:
- <scope1>
- <scope2>
- ...
attributes:
- key: Attribute1-name
value: Attribute1-value (single value or array)
- key: Attribute2-name
value: Attribute2-value (single value or array)
certfile: <optional, the certificate file to use under keys/>

`config/clients.yml` as described [in the Wiki](https://github.com/Fraunhofer-AISEC/omejdn-server/wiki/clients.yml---The-client-database).

### Adding a user

Expand Down
9 changes: 9 additions & 0 deletions config/omejdn.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
---
# The Omejdn host
host: http://localhost:4567
# A base path
path_prefix: ''
# Which address to bind to
bind_to: 0.0.0.0
# Allow Origin Header field
allow_origin: "*"

# Set this to `production` to disable debug output
app_env: debug

# Enable OpenID funtionality
openid: true
Expand Down
6 changes: 3 additions & 3 deletions lib/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def self.extract_jwt_cid(jwt)

def self.find_by_jwt(jwt)
clients = load_clients
puts "looking for client of #{jwt}" if ENV['APP_ENV'] != 'production'
puts "looking for client of #{jwt}" if Config.base_config['app_env'] != 'production'
jwt_alg, jwt_cid = extract_jwt_cid jwt
return nil if jwt_cid.nil?

Expand All @@ -69,12 +69,12 @@ def self.find_by_jwt(jwt)

puts "Client #{jwt_cid} found"
# Try verify
aud = ENV['OMEJDN_JWT_AUD_OVERRIDE'] || ENV['HOST'] || Config.base_config['host']
aud = ENV['OMEJDN_JWT_AUD_OVERRIDE'] || Config.base_config['host']
JWT.decode jwt, client.certificate&.public_key, true,
{ nbf_leeway: 30, aud: aud, verify_aud: true, algorithm: jwt_alg }
return client
rescue StandardError => e
puts "Tried #{client.name}: #{e}" if ENV['APP_ENV'] != 'production'
puts "Tried #{client.name}: #{e}" if Config.base_config['app_env'] != 'production'
return nil
end
puts "ERROR: Client #{jwt_cid} does not exist"
Expand Down
5 changes: 4 additions & 1 deletion lib/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ def self.base_config
def self.base_config=(config)
# Make sure those are integers
config['token']['expiration'] = config['token']['expiration'].to_i
config['id_token']['expiration'] = config['token']['expiration'].to_i
if config['id_token'] && config['id_token']['expiration']
config['id_token']['expiration'] =
config['id_token']['expiration'].to_i
end
write_config OMEJDN_BASE_CONFIG_FILE, config.to_yaml
end

Expand Down
41 changes: 23 additions & 18 deletions lib/token_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class OAuth2Error < RuntimeError
class TokenHelper
def server_key; end

def self.build_access_token_stub(attrs, client, scopes, resources)
def self.build_access_token_stub(attrs, client, scopes, resources, claims)
base_config = Config.base_config
now = Time.new.to_i
{
Expand All @@ -29,18 +29,18 @@ def self.build_access_token_stub(attrs, client, scopes, resources)
'jti' => Base64.urlsafe_encode64(rand(2**64).to_s),
'exp' => now + base_config['token']['expiration'],
'client_id' => client.client_id
}.merge(map_claims_to_userinfo(attrs, [], client, scopes))
}.merge(map_claims_to_userinfo(attrs, claims, client, scopes))
end

# Builds a JWT access token for client including scopes and attributes
def self.build_access_token(client, scopes, resources, user)
def self.build_access_token(client, scopes, resources, user, claims)
# Use user attributes if we have a user context, else use client
# attributes.
if user
new_payload = build_access_token_stub(user.attributes, client, scopes, resources)
new_payload = build_access_token_stub(user.attributes, client, scopes, resources, claims)
new_payload['sub'] = user.username
else
new_payload = build_access_token_stub(client.attributes, client, scopes, resources)
new_payload = build_access_token_stub(client.attributes, client, scopes, resources, claims)
new_payload['sub'] = client.client_id if user.nil?
end
JWT.encode new_payload, Server.load_key('token'), 'RS256', { typ: 'at+jwt', kid: 'default' }
Expand All @@ -62,21 +62,26 @@ def self.add_jwt_claim(jwt_body, key, value)

def self.map_claims_to_userinfo(attrs, claims, client, scopes)
new_payload = {}
attrs.each do |attribute|
# Add attribute if it was specifically requested through OIDC
# claims parameter.
if !claims.empty? &&
claims.key?(attribute['key']) &&
!claims[attribute['key']].nil?
add_jwt_claim(new_payload, attribute['key'], attribute['value'])
next
end

# Add attribute if it was requested indirectly through OIDC
# scope and scope is allowed for client.
next unless client.allowed_scoped_attributes(scopes).include?(attribute['key'])
# Add attribute if it was requested indirectly through OIDC
# scope and scope is allowed for client.
allowed_scoped_attrs = client.allowed_scoped_attributes(scopes)
attrs.select { |a| allowed_scoped_attrs.include?(a['key']) }
.each { |a| add_jwt_claim(new_payload, a['key'], a['value']) }
return new_payload if claims.empty?

# Add attribute if it was specifically requested through OIDC
# claims parameter.
attrs.each do |attr|
next unless claims.key?(attr['key']) && !claims[attr['key']].nil?

add_jwt_claim(new_payload, attribute['key'], attribute['value'])
if attr['dynamic'] && claims[attr['key']]['value']
add_jwt_claim(new_payload, attr['key'], claims[attr['key']]['value'])
elsif attr['dynamic'] && claims[attr['key']]['values']
add_jwt_claim(new_payload, attr['key'], claims[attr['key']]['values'][0])
elsif attr['value']
add_jwt_claim(new_payload, attr['key'], attr['value'])
end
end
new_payload
end
Expand Down
Loading