Skip to content
Draft
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
10 changes: 5 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
cognito_token_verifier (0.5.0)
cognito_token_verifier (0.6.0)
activesupport (>= 5.2)
json-jwt (~> 1.11)
rest-client (~> 2.0)
Expand Down Expand Up @@ -39,7 +39,7 @@ GEM
unf (>= 0.0.5, < 1.0.0)
erubi (1.10.0)
http-accept (1.7.0)
http-cookie (1.0.3)
http-cookie (1.0.5)
domain_name (~> 0.5)
i18n (1.9.1)
concurrent-ruby (~> 1.0)
Expand All @@ -51,9 +51,9 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
method_source (1.0.0)
mime-types (3.3.1)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.0512)
mime-types-data (3.2022.0105)
mini_portile2 (2.8.0)
minitest (5.15.0)
netrc (0.11.0)
Expand Down Expand Up @@ -103,7 +103,7 @@ GEM
concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unf_ext (0.0.8.2)
zeitwerk (2.5.4)

PLATFORMS
Expand Down
44 changes: 29 additions & 15 deletions lib/cognito_token_verifier/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,41 @@ def any_token_use?
['all', 'any', ['id', 'access']].any?{|usage| usage == token_use }
end

def allow_expired_tokens?
allow_expired_tokens
def validate!
raise ConfigSetupError.new(self) unless aws_region.present? and user_pool_id.present?
end

def jwks
begin
raise ConfigSetupError.new(self) unless aws_region.present? and user_pool_id.present?
@jwks ||= JSON.parse(RestClient.get(jwk_url))
rescue RestClient::Exception, JSON::JSONError => e
raise JWKFetchError
end
def allow_expired_tokens?
allow_expired_tokens
end

def iss
"https://cognito-idp.#{aws_region}.amazonaws.com/#{user_pool_id}"
end
# def jwks
# begin
# raise ConfigSetupError.new(self) unless aws_region.present? and user_pool_id.present?
# @jwks ||= JSON.parse(RestClient.get(jwk_url))
# rescue RestClient::Exception, JSON::JSONError => e
# raise JWKFetchError
# end
# end

# TODO Because the load balancer does not encrypt the user claims, we
# recommend that you configure the target group to use HTTPS. If you
# configure your target group to use HTTP, be sure to restrict the traffic
# to your load balancer using security groups. We also recommend that you
# verify the signature before doing any authorization based on the claims.
# To get the public key, get the key ID from the JWT header and use it to
# look up the public key from the endpoint. The endpoint for each AWS Region
# is as follows:
# https://public-keys.auth.elb.region.amazonaws.com/key-id
# See: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html
# def iss
# "https://cognito-idp.#{aws_region}.amazonaws.com/#{user_pool_id}"
# end

private

def jwk_url
"#{iss}/.well-known/jwks.json"
end
# def jwk_url
# "#{iss}/.well-known/jwks.json"
# end
end
end
9 changes: 7 additions & 2 deletions lib/cognito_token_verifier/controller_macros.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ module ControllerMacros

def cognito_token
return @cognito_token if @cognito_token.present? # Caching here, so gem user can access token themselves for additional checks
raise TokenMissing unless request.headers['authorization'].present?
@cognito_token = CognitoTokenVerifier::Token.new(request.headers['authorization'])
@cognito_token = CognitoTokenVerifier::Token.new(find_jwt_claims!)
end

# Support cognito authentication off loaded by ALB
# https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html
def find_jwt_claims!
request.headers['x-amzn-oidc-data'].presence || request.headers['authorization'].presence || raise(TokenMissing)
end

def verify_cognito_token
Expand Down
5 changes: 3 additions & 2 deletions lib/cognito_token_verifier/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ def message

class InvalidIss < CognitoTokenVerifier::Error
def initialize(token)
@iss = token.decoded_token['iss']
@iss = token.iss
@decoded_iss = token.decoded_token['iss']
end

def message
"Invalid token ISS reference. Received #{@iss} while expecting #{CognitoTokenVerifier.config.iss}."
"Invalid token ISS reference. Received #{@decoded_iss} while expecting #{@iss}."
end
end
end
50 changes: 43 additions & 7 deletions lib/cognito_token_verifier/token.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

module CognitoTokenVerifier
class Token
attr_reader :header, :decoded_token
attr_reader :encoded_jwt, :header, :idp_type

def initialize(jwt)
begin
@header= JSON.parse(Base64.decode64(jwt.split('.')[0]))
@jwk = JSON::JWK.new(CognitoTokenVerifier.config.jwks["keys"].detect{|jwk| jwk['kid'] == header['kid']})
@decoded_token = JSON::JWT.decode(jwt, @jwk)
rescue JSON::JWS::VerificationFailed, JSON::JSONError => e
@encoded_jwt = jwt
@header = JSON.parse(Base64.decode64(jwt.split('.')[0]))
@idp_type = header["signer"]&.include?("arn:aws:elasticloadbalancing") ? :alb : :cognito
rescue JSON::JSONError
raise TokenDecodingError
end

def decoded_token
@decoded_token ||= begin
@jwk = JSON::JWK.new(jwks["keys"].detect{|jwk| jwk['kid'] == header['kid']})
JSON::JWT.decode(encoded_jwt, @jwk)
rescue JSON::JWS::VerificationFailed
raise TokenDecodingError
end
end
Expand All @@ -18,12 +25,41 @@ def expired?
decoded_token['exp'] < Time.now.to_i and not CognitoTokenVerifier.config.allow_expired_tokens?
end

def jwks
CognitoTokenVerifier.config.validate!
@jwks ||= JSON.parse(RestClient.get(jwk_url))
rescue RestClient::Exception, JSON::JSONError => e
raise JWKFetchError
end

def jwk_url
case idp_type
when :alb
"#{iss}/#{header['kid']}"
else
"#{iss}/.well-known/jwks.json"
end
end

def iss
case idp_type
when :alb
"https://public-keys.auth.elb.#{CognitoTokenVerifier.config.aws_region}.amazonaws.com"
else
"https://cognito-idp.#{CognitoTokenVerifier.config.aws_region}.amazonaws.com/#{CognitoTokenVerifier.config.user_pool_id}"
end
end

def valid_token_use?
CognitoTokenVerifier.config.any_token_use? || [CognitoTokenVerifier.config.token_use].flatten.include?(decoded_token['token_use'])
end

def alb_claim?
idp_type == :alb
end

def valid_iss?
decoded_token['iss'] == CognitoTokenVerifier.config.iss
decoded_token['iss'] == iss
end
end
end
21 changes: 21 additions & 0 deletions spec/cognito_token_verifier/token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
expect(subject.decoded_token).to_not be_nil
end

it "sets the correct idp type" do
expect(subject.idp_type).to be :cognito
end

describe "#expired?" do
it "returns true when the token is expired" do
expect(subject.expired?).to be true
Expand All @@ -37,4 +41,21 @@
expect(subject.valid_token_use?).to be false
end
end


context "ALB terminated cognito" do
subject { CognitoTokenVerifier::Token.new(COGNITO_ALB_TEST_TOKEN) }

before :each do
allow_any_instance_of(RestClient).to receive(:get).and_return({})
end

it "provides access to the decoded token header" do
expect(subject.header).to_not be_nil
end

it "sets the correct idp type" do
expect(subject.idp_type).to be :alb
end
end
end
10 changes: 10 additions & 0 deletions spec/fixtures/constants.rb
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
COGNITO_TEST_TOKEN = "eyJraWQiOiJhYXN4VGgycHF0YkFJVDErb0xvZVZoY0dWWVY4UVVhZ0JtOG9CZHN5WWlrPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI1MWFhNjIzNS0xNGU5LTRlNjctODI4Yy1hNmQ0NGEyMTJmN2EiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiY3VzdG9tOkVtcGxveWVlIjoiMCIsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yXzk0S1hWMjdyciIsImNvZ25pdG86dXNlcm5hbWUiOiI1MWFhNjIzNS0xNGU5LTRlNjctODI4Yy1hNmQ0NGEyMTJmN2EiLCJhdWQiOiIyZXNjNTRvbGFmaXJrYmNzMmZkYTJjYjEzMSIsImV2ZW50X2lkIjoiMzU2ZWZlNzMtMDQ3Mi0xMWU5LWE0YmUtODk5MDg1NjkzYTljIiwidG9rZW5fdXNlIjoiaWQiLCJjdXN0b206UGFyZW50IjoiMCIsImF1dGhfdGltZSI6MTU0NTMyMjQxOCwiY3VzdG9tOkFkbWluIjoiMSIsImV4cCI6MTU0NTMyNjAxOCwiaWF0IjoxNTQ1MzIyNDE4LCJlbWFpbCI6ImJpbGxAa2FuZ2Fyb290aW1lLmNvbSJ9.I-bFL_d0A0yyxslqpxrHED8iNLBm_DrYCKubgxzmAKb3D_MyKfYUw6AjjznLzb4Q1rYoe-JwAWH1Ms0Dt2H8hQeApCdODvl9NZOgsr7Vh9JxprXehT3IcC1zXCLvlsWlX81kCvvg_moUg9rBd5LuBtLvXAs6YIjG304stmCGczhq6Da29AzJYZd8elGR24DyRLWBuDCI_8v3mKsPd3h2NcVqhr-KiM74X5nNqZTB5Rj2sFZJLdgqpZbRlkDeidukzFJTWpW9ZVc_9J6Qls34TNYL4myS6UALS9RtbAlfopphg2GMTwjsAfakj9HnUglxFuCOrp_pwptyHGdYy7QcUQ".freeze
COGNITO_ALB_TEST_TOKEN = "ewogICAiYWxnIjogImFsZ29yaXRobSIsCiAgICJraWQiOiAiMTIzNDU2NzgtMTIzNC0xMjM0LTEyMzQtMTIzNDU2Nzg5MDEyIiwKICAgInNpZ25lciI6ICJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nOnJlZ2lvbi1jb2RlOmFjY291bnQtaWQ6bG9hZGJhbGFuY2VyL2FwcC9sb2FkLWJhbGFuY2VyLW5hbWUvbG9hZC1iYWxhbmNlci1pZCIsIAogICAiaXNzIjogInVybCIsCiAgICJjbGllbnQiOiAiY2xpZW50LWlkIiwKICAgImV4cCI6ICJleHBpcmF0aW9uIgp9Cg".freeze

# {
# "alg": "algorithm",
# "kid": "12345678-1234-1234-1234-123456789012",
# "signer": "arn:aws:elasticloadbalancing:region-code:account-id:loadbalancer/app/load-balancer-name/load-balancer-id",
# "iss": "url",
# "client": "client-id",
# "exp": "expiration"
# }