summaryrefslogtreecommitdiff
path: root/lib/atlassian/jira_connect/jwt/asymmetric.rb
blob: 8698be70eb9c0fcc7d1f0bcc39a505a481ccd1e5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# 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'
        DEFAULT_PUBLIC_KEY_CDN_URL = 'https://connect-install-keys.atlassian.com'
        PROXY_PUBLIC_KEY_PATH = '/-/jira_connect/public_keys'
        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

        def public_key_cdn_url
          public_key_cdn_url_setting.presence || DEFAULT_PUBLIC_KEY_CDN_URL
        end

        def public_key_cdn_url_setting
          @public_key_cdn_url_setting ||=
            if Gitlab::CurrentSettings.jira_connect_proxy_url.present?
              Gitlab::Utils.append_path(Gitlab::CurrentSettings.jira_connect_proxy_url, PROXY_PUBLIC_KEY_PATH)
            end
        end
      end
    end
  end
end