-
Notifications
You must be signed in to change notification settings - Fork 68
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
Add ID token validation #62
Conversation
de8b38e
to
c000825
Compare
lib/omniauth/auth0/jwt_validator.rb
Outdated
attr_accessor :issuer, :jwks | ||
|
||
# Initializer | ||
# @param options class |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Options "class"? What does that mean here? Should it say object?
end | ||
|
||
# Docs: https://github.com/jwt/ruby-jwt#add-custom-header-fields | ||
decode_options = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4 space indentation - should be 2
# @return hash - The parsed head of the JWT passed, empty hash if not. | ||
def token_head(jwt) | ||
jwt_parts = jwt.split('.') | ||
return {} if blank?(jwt_parts) || blank?(jwt_parts[0]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you return {}
from here, can the rest of the library execute? Or will this get caught in the else
clause of decode
and raise the JWT::VerificationError
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would you name this line if you had to give it a name? What are we checking for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you return {} from here, can the rest of the library execute?
The part of this lib that uses it will fail on looking for an algorithm, which seems rational to me for an empty head.
What would you name this line if you had to give it a name?
Not sure what you're asking here.
What are we checking for?
Whether or not was have something to parse.
lib/omniauth/auth0/jwt_validator.rb
Outdated
# @return string - The @domain property as a URL. | ||
def domain_url | ||
domain_url = URI(@domain) | ||
domain_url = URI("https://#{@domain}") if domain_url.scheme.nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line and the one above seem strange. A conditional may make more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you rephrase? This allows a URI to be loaded without http://
or https://
and will convert it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure! So here you're assigning to domain_url
twice (you're also shadowing the method name itself which is confusing) and I get why, it just feels odd. However, I think moving this up to the constructor would make sense anyway. For instance:
def initialize(options)
@issuer = "#{domain_url}/"
@client_id = options.client_id
@client_secret = options.client_secret
@jwks = nil
temp_domain = URI(options.domain)
@domain = temp_domain.scheme? ? temp_domain : URI("https://#{options.domain}")
end
Then this method can simply return @domain.to_s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@machuga - Good comment. I ended up removing this method and the @domain
property since it was only used for @issuer
.
lib/omniauth/auth0/jwt_validator.rb
Outdated
def jwks_key(key, kid) | ||
@jwks ||= fetch_jwks | ||
return nil if blank?(@jwks[:keys]) | ||
@jwks[:keys].each do |jwk| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should be able to shorten this down to:
def jwks_key(key, kid)
@jwks ||= fetch_jwks
return nil if blank?(@jwks[:keys])
@jwks[:keys].find { |key| jwk[:kid] == kid }
end
However, you're memoizing @jwks
here in a method that isn't named jwks
or directly related and that makes me a bit uncomfortable. Maybe it should fetch_jwks
should be the location where this is memoized? Maybe the flow could be a bit different?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree RE: where this is memoized so I moved that to fetch_jwks
. But I think you're misunderstanding what this is doing. The JWKS is like:
{
keys [
{
"kid" : "kid_1",
"key" : "thing_we_want"
}, {
"kid" : "kid_2",
"key" : "thing_we_want"
}
]
}
... in the general case so we need to iterate through the keys, check the kid
for each, and return the value if we have a matching kid
. Otherwise return nil. I simplified it a bit based on your feedback but what you're proposing won't do what we need here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm mostly just being picky about the Ruby idioms in this situation!
lib/omniauth/auth0/jwt_validator.rb
Outdated
# Get a JWKS from the issuer | ||
# @param path string - JWKS path from issuer. | ||
# @return void | ||
def fetch_jwks(path = '.well-known/jwks.json') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intended to be a public method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was but I think it should be private in this case.
LGTM from a security perspective |
6fc441f
to
e4e0e46
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Last few requests and then I think we'll be good!
lib/omniauth/auth0/jwt_validator.rb
Outdated
|
||
# Get a JWKS from the issuer | ||
# @return void | ||
def fetch_jwks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what I was trying to point out earlier about this method name was that if you keep it as jwks
you can then us this memoized form around the app, like above in jwks_key
:
def jwks_key(key, kid)
return nil if blank?(jwks[:keys])
jwks[:keys].each { |jwk| return jwk[key] if jwk[:kid] == kid }
end
That way you're never concerned with when/if the fetching is done.
lib/omniauth/auth0/jwt_validator.rb
Outdated
def jwks_key(key, kid) | ||
fetch_jwks | ||
return nil if blank?(@jwks[:keys]) | ||
@jwks[:keys].each { |jwk| return jwk[key] if jwk[:kid] == kid } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this method may look a bit more clear as:
def jwks_key(key, kid)
return nil if blank?(jwks[:keys])
matching_jwk = jwks[:keys].find { |jwk| jwk[:kid] == kid }
matching_jwk[key] if matching_jwk
end
This way uses a clear method name/block to indicate "I'm looking for a certain key, then going to perform an operation with what I found"
lib/omniauth/strategies/auth0.rb
Outdated
credentials do | ||
hash = { 'token' => access_token.token } | ||
hash = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A more idiomatic way of handling the hash assignment in this block may be:
credentials do
# Not sure of the name `hash` here...what is its purpose?
hash = {
'token' => access_token.token,
'expires' => true
}
if access_token.params
hash.merge!({
'id_token' => access_token.params['id_token'],
'token_type' => access_token.params['token_type'],
'refresh_token' => access_token.refresh_token,
})
end
# Make sure the ID token can be verified and decoded.
auth0_jwt = OmniAuth::Auth0::JWTValidator.new(options)
fail!(:invalid_id_token) unless auth0_jwt.decode(hash['id_token']).length
hash
end
e4e0e46
to
bf84621
Compare
This PR adds ID token validation to the basic OmniAuth-Auth0 strategy. The main strategy was not altered in any way functionally except to add this validation (minor docs additions and minor formatting). The main work for this PR is in the new
OmniAuth::Auth0::JWTValidator
class.This uses the Ruby JWT library and validates the following: