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

Parsing claims from the id_token [sdk-2247] #120

Merged
merged 1 commit into from
Jan 20, 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
39 changes: 26 additions & 13 deletions lib/omniauth/auth0/jwt_validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,33 @@ def initialize(options, authorize_params = {})
end

# Verify a token's signature. Only tokens signed with the RS256 or HS256 signatures are supported.
# Deprecated: Please use `decode` instead
# @return array - The token's key and signing algorithm
def verify_signature(jwt)
head = token_head(jwt)

# Make sure the algorithm is supported and get the decode key.
if head[:alg] == 'RS256'
key, alg = [rs256_decode_key(head[:kid]), head[:alg]]
elsif head[:alg] == 'HS256'
key, alg = [@client_secret, head[:alg]]
else
raise OmniAuth::Auth0::TokenValidationError.new("Signature algorithm of #{head[:alg]} is not supported. Expected the ID token to be signed with RS256 or HS256")
end
key, alg = extract_key(head)

# Call decode to verify the signature
JWT.decode(jwt, key, true, decode_opts(alg))

return key, alg
end

# Decodes a JWT and verifies it's signature. Only tokens signed with the RS256 or HS256 signatures are supported.
# @param jwt string - JWT to verify.
# @return hash - The decoded token, if there were no exceptions.
# @see https://github.com/jwt/ruby-jwt
def decode(jwt)
head = token_head(jwt)
key, alg = extract_key(head)

# Call decode to verify the signature
JWT.decode(jwt, key, true, decode_opts(alg))
end

# Verify a JWT.
# @param jwt string - JWT to verify.
# @param authorize_params hash - Authorization params to verify on the JWT
# @return hash - The verified token, if there were no exceptions.
# @return hash - The verified token payload, if there were no exceptions.
def verify(jwt, authorize_params = {})
if !jwt
raise OmniAuth::Auth0::TokenValidationError.new('ID token is required but missing')
Expand All @@ -62,8 +66,7 @@ def verify(jwt, authorize_params = {})
raise OmniAuth::Auth0::TokenValidationError.new('ID token could not be decoded')
end

key, alg = verify_signature(jwt)
id_token, header = JWT.decode(jwt, key, false)
id_token, header = decode(jwt)
verify_claims(id_token, authorize_params)

return id_token
Expand Down Expand Up @@ -116,6 +119,16 @@ def decode_opts(alg)
}
end

def extract_key(head)
if head[:alg] == 'RS256'
key, alg = [rs256_decode_key(head[:kid]), head[:alg]]
elsif head[:alg] == 'HS256'
key, alg = [@client_secret, head[:alg]]
else
raise OmniAuth::Auth0::TokenValidationError.new("Signature algorithm of #{head[:alg]} is not supported. Expected the ID token to be signed with RS256 or HS256")
end
end

def rs256_decode_key(kid)
jwks_x5c = jwks_key(:x5c, kid)

Expand Down
19 changes: 15 additions & 4 deletions lib/omniauth/strategies/auth0.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,7 @@ def client
auth_scope = session_authorize_params[:scope]
if auth_scope.respond_to?(:include?) && auth_scope.include?('openid')
# Make sure the ID token can be verified and decoded.
auth0_jwt = OmniAuth::Auth0::JWTValidator.new(options)
auth0_jwt.verify(credentials['id_token'], session_authorize_params)
jwt_validator.verify(credentials['id_token'], session_authorize_params)
end

credentials
Expand Down Expand Up @@ -130,11 +129,23 @@ def callback_phase
end

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

# Parse the raw user info.
def raw_info
userinfo_url = options.client_options.userinfo_url
@raw_info ||= access_token.get(userinfo_url).parsed
return @raw_info if @raw_info

if access_token["id_token"]
claims, header = jwt_validator.decode(access_token["id_token"])
@raw_info = claims
else
userinfo_url = options.client_options.userinfo_url
@raw_info = access_token.get(userinfo_url).parsed
end

return @raw_info
end

# Check if the options include a client_id
Expand Down
39 changes: 22 additions & 17 deletions spec/omniauth/strategies/auth0_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@
end

describe 'client_options' do
let(:subject) { auth0.client }
let(:subject) { OmniAuth::Strategies::Auth0.new(
application,
client_id,
client_secret,
domain_url
).client }

context 'domain with https' do
let(:domain_url) { 'https://samples.auth0.com' }
Expand Down Expand Up @@ -168,12 +173,17 @@
payload['sub'] = user_id
payload['iss'] = "#{domain_url}/"
payload['aud'] = client_id
payload['name'] = name
payload['nickname'] = nickname
payload['picture'] = picture
payload['email'] = email
payload['email_verified'] = email_verified

JWT.encode payload, client_secret, 'HS256'
end

let(:oauth_response) do
{
id_token: id_token,
access_token: access_token,
expires_in: expires_in,
token_type: token_type
Expand All @@ -189,17 +199,7 @@
}
end

let(:basic_user_info) { { sub: user_id } }
let(:oidc_user_info) do
{
sub: user_id,
name: name,
nickname: nickname,
email: email,
picture: picture,
email_verified: email_verified
}
end
let(:basic_user_info) { { "sub" => user_id, "name" => name } }

def stub_auth(body)
stub_request(:post, 'https://samples.auth0.com/oauth/token')
Expand Down Expand Up @@ -227,7 +227,9 @@ def trigger_callback
WebMock.reset!
end

let(:subject) { MultiJson.decode(last_response.body) }
let(:subject) do
MultiJson.decode(last_response.body)
end

context 'basic oauth' do
before do
Expand All @@ -246,10 +248,14 @@ def trigger_callback
expect(subject['credentials']['expires_at']).to_not be_nil
end

it 'has basic values' do
it 'has basic values' do
expect(subject['provider']).to eq('auth0')
expect(subject['uid']).to eq(user_id)
expect(subject['info']['name']).to eq(user_id)
expect(subject['info']['name']).to eq(name)
end

it 'should use the user info endpoint' do
expect(subject['extra']['raw_info']).to eq(basic_user_info)
end
end

Expand All @@ -275,7 +281,6 @@ def trigger_callback
context 'oidc' do
before do
stub_auth(oidc_response)
stub_userinfo(oidc_user_info)
trigger_callback
end

Expand Down