lib/omniauth/auth0/jwt_validator.rb in omniauth-auth0-2.2.0 vs lib/omniauth/auth0/jwt_validator.rb in omniauth-auth0-2.3.0
- old
+ new
@@ -13,38 +13,54 @@
# @param options object
# options.domain - Application domain.
# options.issuer - Application issuer (optional).
# options.client_id - Application Client ID.
# options.client_secret - Application Client Secret.
- def initialize(options)
+
+ def initialize(options, authorize_params = {})
@domain = uri_string(options.domain)
# Use custom issuer if provided, otherwise use domain
@issuer = @domain
@issuer = uri_string(options.issuer) if options.respond_to?(:issuer)
@client_id = options.client_id
@client_secret = options.client_secret
end
- # Decode a JWT.
- # @param jwt string - JWT to decode.
- # @return hash - The decoded token, if there were no exceptions.
- # @see https://github.com/jwt/ruby-jwt
- def decode(jwt)
+ def verify_signature(jwt)
head = token_head(jwt)
# Make sure the algorithm is supported and get the decode key.
- decode_key = @client_secret
if head[:alg] == 'RS256'
- decode_key = rs256_decode_key(head[:kid])
- elsif head[:alg] != 'HS256'
- raise JWT::VerificationError, :id_token_alg_unsupported
+ [rs256_decode_key(head[:kid]), head[:alg]]
+ elsif head[:alg] == 'HS256'
+ [@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
- # Docs: https://github.com/jwt/ruby-jwt#algorithms-and-usage
- JWT.decode(jwt, decode_key, true, decode_opts(head[:alg]))
+ # 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.
+ def verify(jwt, authorize_params = {})
+ if !jwt
+ raise OmniAuth::Auth0::TokenValidationError.new('ID token is required but missing')
+ end
+
+ parts = jwt.split('.')
+ if parts.length != 3
+ 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)
+ verify_claims(id_token, authorize_params)
+
+ return id_token
end
# Get the decoded head segment from a JWT.
# @return hash - The parsed head of the JWT passed, empty hash if not.
def token_head(jwt)
@@ -74,31 +90,17 @@
matching_jwk = jwks[:keys].find { |jwk| jwk[:kid] == kid }
matching_jwk[key] if matching_jwk
end
private
-
- # Get the JWT decode options
- # Docs: https://github.com/jwt/ruby-jwt#add-custom-header-fields
- # @return hash
- def decode_opts(alg)
- {
- algorithm: alg,
- leeway: 30,
- verify_expiration: true,
- verify_iss: true,
- iss: @issuer,
- verify_aud: true,
- aud: @client_id,
- verify_not_before: true
- }
- end
-
def rs256_decode_key(kid)
jwks_x5c = jwks_key(:x5c, kid)
- raise JWT::VerificationError, :jwks_missing_x5c if jwks_x5c.nil?
+ if jwks_x5c.nil?
+ raise OmniAuth::Auth0::TokenValidationError.new("Could not find a public key for Key ID (kid) '#{kid}''")
+ end
+
jwks_public_cert(jwks_x5c.first)
end
# Get a JWKS from the domain
# @return void
@@ -126,9 +128,100 @@
# @return string
def uri_string(uri)
temp_domain = URI(uri)
temp_domain = URI("https://#{uri}") unless temp_domain.scheme
"#{temp_domain}/"
+ end
+
+ def verify_claims(id_token, authorize_params)
+ leeway = authorize_params[:leeway] || 60
+ max_age = authorize_params[:max_age]
+ nonce = authorize_params[:nonce]
+
+ verify_iss(id_token)
+ verify_sub(id_token)
+ verify_aud(id_token)
+ verify_expiration(id_token, leeway)
+ verify_iat(id_token)
+ verify_nonce(id_token, nonce)
+ verify_azp(id_token)
+ verify_auth_time(id_token, leeway, max_age)
+ end
+
+ def verify_iss(id_token)
+ issuer = id_token['iss']
+ if !issuer
+ raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim must be a string present in the ID token")
+ elsif @issuer != issuer
+ raise OmniAuth::Auth0::TokenValidationError.new("Issuer (iss) claim mismatch in the ID token, expected (#{@issuer}), found (#{id_token['iss']})")
+ end
+ end
+
+ def verify_sub(id_token)
+ subject = id_token['sub']
+ if !subject || !subject.is_a?(String) || subject.empty?
+ raise OmniAuth::Auth0::TokenValidationError.new('Subject (sub) claim must be a string present in the ID token')
+ end
+ end
+
+ def verify_aud(id_token)
+ audience = id_token['aud']
+ if !audience || !(audience.is_a?(String) || audience.is_a?(Array))
+ raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim must be a string or array of strings present in the ID token")
+ elsif audience.is_a?(Array) && !audience.include?(@client_id)
+ raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but was not one of #{audience.join(', ')}")
+ elsif audience.is_a?(String) && audience != @client_id
+ raise OmniAuth::Auth0::TokenValidationError.new("Audience (aud) claim mismatch in the ID token; expected #{@client_id} but found #{audience}")
+ end
+ end
+
+ def verify_expiration(id_token, leeway)
+ expiration = id_token['exp']
+ if !expiration || !expiration.is_a?(Integer)
+ raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim must be a number present in the ID token")
+ elsif expiration <= Time.now.to_i - leeway
+ raise OmniAuth::Auth0::TokenValidationError.new("Expiration time (exp) claim error in the ID token; current time (#{Time.now}) is after expiration time (#{Time.at(expiration + leeway)})")
+ end
+ end
+
+ def verify_iat(id_token)
+ if !id_token['iat']
+ raise OmniAuth::Auth0::TokenValidationError.new("Issued At (iat) claim must be a number present in the ID token")
+ end
+ end
+
+ def verify_nonce(id_token, nonce)
+ if nonce
+ received_nonce = id_token['nonce']
+ if !received_nonce
+ raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim must be a string present in the ID token")
+ elsif nonce != received_nonce
+ raise OmniAuth::Auth0::TokenValidationError.new("Nonce (nonce) claim value mismatch in the ID token; expected (#{nonce}), found (#{received_nonce})")
+ end
+ end
+ end
+
+ def verify_azp(id_token)
+ audience = id_token['aud']
+ if audience.is_a?(Array) && audience.length > 1
+ azp = id_token['azp']
+ if !azp || !azp.is_a?(String)
+ raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim must be a string present in the ID token when Audience (aud) claim has multiple values")
+ elsif azp != @client_id
+ raise OmniAuth::Auth0::TokenValidationError.new("Authorized Party (azp) claim mismatch in the ID token; expected (#{@client_id}), found (#{azp})")
+ end
+ end
+ end
+
+ def verify_auth_time(id_token, leeway, max_age)
+ if max_age
+ auth_time = id_token['auth_time']
+ if !auth_time || !auth_time.is_a?(Integer)
+ raise OmniAuth::Auth0::TokenValidationError.new("Authentication Time (auth_time) claim must be a number present in the ID token when Max Age (max_age) is specified")
+ elsif Time.now.to_i > auth_time + max_age + leeway;
+ raise OmniAuth::Auth0::TokenValidationError.new("Authentication Time (auth_time) claim in the ID token indicates that too much time has passed since the last end-user authentication. Current time (#{Time.now}) is after last auth time (#{Time.at(auth_time + max_age + leeway)})")
+ end
+ end
end
end
end
end