diff options
Diffstat (limited to 'lib/atlassian/jira_connect/jwt/asymmetric.rb')
-rw-r--r-- | lib/atlassian/jira_connect/jwt/asymmetric.rb | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/lib/atlassian/jira_connect/jwt/asymmetric.rb b/lib/atlassian/jira_connect/jwt/asymmetric.rb new file mode 100644 index 00000000000..0611a17c005 --- /dev/null +++ b/lib/atlassian/jira_connect/jwt/asymmetric.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module Atlassian + module JiraConnect + module Jwt + # See documentation about Atlassian asymmetric JWT verification: + # https://developer.atlassian.com/cloud/jira/platform/understanding-jwt-for-connect-apps/#verifying-a-asymmetric-jwt-token-for-install-callbacks + + class Asymmetric + include Gitlab::Utils::StrongMemoize + + KeyFetchError = Class.new(StandardError) + + ALGORITHM = 'RS256' + PUBLIC_KEY_CDN_URL = 'https://connect-install-keys.atlassian.com/' + UUID4_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze + + def initialize(token, verification_claims) + @token = token + @verification_claims = verification_claims + end + + def valid? + claims.present? && claims['qsh'] == verification_qsh + end + + def iss_claim + return unless claims + + claims['iss'] + end + + private + + def claims + strong_memoize(:claims) do + _, jwt_headers = decode_token + public_key = retrieve_public_key(jwt_headers['kid']) + + decoded_claims(public_key) + rescue JWT::DecodeError, OpenSSL::PKey::PKeyError, KeyFetchError + end + end + + def decoded_claims(public_key) + decode_token( + public_key, + true, + **relevant_claims, + verify_aud: true, + verify_iss: true, + algorithm: ALGORITHM + ).first + end + + def decode_token(key = nil, verify = false, **claims) + Atlassian::Jwt.decode(@token, key, verify, **claims) + end + + def retrieve_public_key(key_id) + raise KeyFetchError unless UUID4_REGEX.match?(key_id) + + public_key = Gitlab::HTTP.try_get(PUBLIC_KEY_CDN_URL + key_id).try(:body) + + raise KeyFetchError if public_key.blank? + + OpenSSL::PKey.read(public_key) + end + + def relevant_claims + @verification_claims.slice(:aud, :iss) + end + + def verification_qsh + @verification_claims[:qsh] + end + end + end + end +end |