Skip to content
Open
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
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,34 @@ Adding the SDK to your Rails app requires a few steps:

Create the file `./config/auth0.yml` within your application directory with the following content:

### For client secret authentication

```yml
development:
auth0_domain: <YOUR_DOMAIN>
auth0_client_id: <YOUR_CLIENT_ID>
auth0_client_secret: <YOUR AUTH0 CLIENT SECRET>
```

#### For client assertion signing key authentication

```yml
development:
auth0_domain: <YOUR_DOMAIN>
auth0_client_id: <YOUR_CLIENT_ID>
auth0_client_assertion_signing_key: <YOUR AUTH0 CLIENT ASSERTION SIGNING PRIVATE KEY>
auth0_client_assertion_signing_algorithm: <YOUR AUTH0 CLIENT ASSERTION SIGNING ALGORITHM>
```
**Note**: you must upload the corresponding public key to your Auth0 tenant, so that Auth0 is able to verify the JWT signature.

client_assertion_siging_algorithm is optional and defaults to RS256.

### Create the initializer

Create a new Ruby file in `./config/initializers/auth0.rb` to configure the OmniAuth middleware:

### For client secret authentication

```ruby
AUTH0_CONFIG = Rails.application.config_for(:auth0)

Expand All @@ -81,6 +98,27 @@ Rails.application.config.middleware.use OmniAuth::Builder do
end
```

#### For client assertion signing key authentication

```ruby
AUTH0_CONFIG = Rails.application.config_for(:auth0)

Rails.application.config.middleware.use OmniAuth::Builder do
provider(
:auth0,
AUTH0_CONFIG['auth0_client_id'],
nil,
AUTH0_CONFIG['auth0_domain'],
callback_path: '/auth/auth0/callback',
authorize_params: {
scope: 'openid profile'
}
client_assertion_signing_key: AUTH0_CONFIG[:auth0_client_assertion_signing_key],
client_assertion_signing_algorithm: AUTH0_CONFIG[:auth0_client_assertion_signing_algorithm]}
)
end
```

### Create the callback controller

Create a new controller `./app/controllers/auth0_controller.rb` to handle the callback from Auth0.
Expand Down
35 changes: 35 additions & 0 deletions lib/omniauth/auth0/jwt_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module OmniAuth
module Auth0
# JWTToken class to generate a JWT token for client assertion
# as per the OAuth 2.0 Client Credentials Grant specification.
class JWTToken
attr_reader :client_id, :domain_url, :client_assertion_signing_key, :client_assertion_signing_algorithm

def initialize(client_id:, domain_url:, client_assertion_signing_key:, client_assertion_signing_algorithm: nil)
@client_id = client_id
@domain_url = domain_url
@client_assertion_signing_key = client_assertion_signing_key
@client_assertion_signing_algorithm = client_assertion_signing_algorithm || 'RS256'
end

def jwt_token
JWT.encode(jwt_payload, client_assertion_signing_key, client_assertion_signing_algorithm)
end

private

def jwt_payload
{
iss: client_id,
sub: client_id,
aud: File.join(domain_url, '/'),
iat: Time.now.utc.to_i,
exp: Time.now.utc.to_i + 60,
jti: SecureRandom.uuid
}
end
end
end
end
4 changes: 2 additions & 2 deletions lib/omniauth/auth0/jwt_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,10 @@ def verify_org(id_token, organization)
if validate_as_id
org_id = id_token['org_id']
if !org_id || !org_id.is_a?(String)
raise OmniAuth::Auth0::TokenValidationError,
raise OmniAuth::Auth0::TokenValidationError,
'Organization Id (org_id) claim must be a string present in the ID token'
elsif org_id != organization
raise OmniAuth::Auth0::TokenValidationError,
raise OmniAuth::Auth0::TokenValidationError,
"Organization Id (org_id) claim value mismatch in the ID token; expected '#{organization}', found '#{org_id}'"
end
else
Expand Down
62 changes: 48 additions & 14 deletions lib/omniauth/strategies/auth0.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'uri'
require 'securerandom'
require 'omniauth-oauth2'
require 'omniauth/auth0/jwt_token'
require 'omniauth/auth0/jwt_validator'
require 'omniauth/auth0/telemetry'
require 'omniauth/auth0/errors'
Expand All @@ -13,6 +14,8 @@ module Strategies
# Auth0 OmniAuth strategy
class Auth0 < OmniAuth::Strategies::OAuth2
include OmniAuth::Auth0::Telemetry
AUTHORIZATION_CODE_GRANT_TYPE = 'authorization_code'
CLIENT_ASSERTION_TYPE = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'

option :name, 'auth0'

Expand All @@ -28,6 +31,8 @@ def client
options.client_options.authorize_url = '/authorize'
options.client_options.token_url = '/oauth/token'
options.client_options.userinfo_url = '/userinfo'
setup_client_options_auth_scheme

super
end

Expand Down Expand Up @@ -100,25 +105,20 @@ def authorize_params
end

def build_access_token
options.token_params.merge!(client_assertion_signing_key_token_params) if client_assertion_signing_key_auth?
options.token_params[:headers] = { 'Auth0-Client' => telemetry_encoded }
super
end

# Declarative override for the request phase of authentication
def request_phase
if no_client_id?
# Do we have a client_id for this Application?
fail!(:missing_client_id)
elsif no_client_secret?
# Do we have a client_secret for this Application?
fail!(:missing_client_secret)
elsif no_domain?
# Do we have a domain for this Application?
fail!(:missing_domain)
else
# All checks pass, run the Oauth2 request_phase method.
super
end
return fail!(:missing_client_id) if no_client_id?
return fail!(:missing_client_secret) if no_client_secret?
return fail!(:missing_domain) if no_domain?
return fail!(:missing_client_assertion_signing_key) if no_client_assertion_signing_key?

# All checks pass, run the Oauth2 request_phase method.
super
end

def callback_phase
Expand All @@ -128,10 +128,32 @@ def callback_phase
end

private

def client_assertion_signing_key_auth?
options['client_assertion_signing_key']
end

def client_assertion_signing_key_token_params
{
grant_type: AUTHORIZATION_CODE_GRANT_TYPE,
client_assertion_type: CLIENT_ASSERTION_TYPE,
client_assertion: jwt_token,
audience: domain_url
}
end

def jwt_validator
@jwt_validator ||= OmniAuth::Auth0::JWTValidator.new(options)
end

def jwt_token
OmniAuth::Auth0::JWTToken.new(client_id: options.client_id,
domain_url:,
client_assertion_signing_key: options.client_assertion_signing_key,
client_assertion_signing_algorithm: options.client_assertion_signing_algorithm)
.jwt_token
end

# Parse the raw user info.
def raw_info
return @raw_info if @raw_info
Expand All @@ -154,20 +176,32 @@ def no_client_id?

# Check if the options include a client_secret
def no_client_secret?
['', nil].include?(options.client_secret)
['', nil].include?(options.client_secret) && !options.key?('client_assertion_signing_key')
end

# Check if the options include a domain
def no_domain?
['', nil].include?(options.domain)
end

# Check if the options include a client_assertion_signing_key
def no_client_assertion_signing_key?
options.key?('client_assertion_signing_key') && ['', nil].include?(options.client_assertion_signing_key)
end

# Normalize a domain to a URL.
def domain_url
domain_url = URI(options.domain)
domain_url = URI("https://#{domain_url}") if domain_url.scheme.nil?
domain_url.to_s
end

# Setup the auth_scheme for the client options if using client assertion signing key
def setup_client_options_auth_scheme
return unless client_assertion_signing_key_auth?

options.client_options.auth_scheme = :private_key_jwt
end
end
end
end
47 changes: 47 additions & 0 deletions spec/omniauth/auth0/jwt_token_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require 'spec_helper'
require 'json'
require 'jwt'

describe OmniAuth::Auth0::JWTToken do
let(:client_id) { 'CLIENT_ID' }
let(:domain_url) { 'https://samples.auth0.com' }
let(:client_assertion_signing_key) { OpenSSL::PKey::RSA.generate(2048) }

describe '#jwt_token' do
it 'generates a valid JWT token' do
uuid = '12345678-1234-5678-1234-567812345678'
allow(SecureRandom).to receive(:uuid).and_return(uuid)

jwt_token = described_class.new(client_id:,
domain_url:,
client_assertion_signing_key:,
client_assertion_signing_algorithm: 'RS256')
.jwt_token
decoded_token = JWT.decode(jwt_token, client_assertion_signing_key, true, { algorithm: 'RS256' })

expect(decoded_token[0]['iss']).to eq(client_id)
expect(decoded_token[0]['sub']).to eq(client_id)
expect(decoded_token[0]['aud']).to eq("#{domain_url}/")
expect(decoded_token[0]['iat']).to be_within(5).of(Time.now.utc.to_i)
expect(decoded_token[0]['exp']).to eq(decoded_token[0]['iat'] + 60)
expect(decoded_token[0]['jti']).to eq(uuid)
end

it 'defaults to RS256 algorithm if not specified' do
uuid = '12345678-1234-5678-1234-567812345678'
allow(SecureRandom).to receive(:uuid).and_return(uuid)

jwt_token = described_class.new(client_id:, domain_url:, client_assertion_signing_key:).jwt_token
decoded_token = JWT.decode(jwt_token, client_assertion_signing_key, true, { algorithm: 'RS256' })

expect(decoded_token[0]['iss']).to eq(client_id)
expect(decoded_token[0]['sub']).to eq(client_id)
expect(decoded_token[0]['aud']).to eq("#{domain_url}/")
expect(decoded_token[0]['iat']).to be_within(5).of(Time.now.utc.to_i)
expect(decoded_token[0]['exp']).to eq(decoded_token[0]['iat'] + 60)
expect(decoded_token[0]['jti']).to eq(uuid)
end
end
end
Loading