File: google_spec.rb

package info (click to toggle)
ruby-oauth2 2.0.9-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 644 kB
  • sloc: ruby: 3,763; makefile: 4; sh: 4
file content (141 lines) | stat: -rw-r--r-- 5,376 bytes parent folder | download
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# frozen_string_literal: true

require 'jwt'

RSpec.describe 'using OAuth2 with Google' do
  # This describes authenticating to a Google API via a service account.
  # See their docs: https://developers.google.com/identity/protocols/OAuth2ServiceAccount

  describe 'via 2-legged JWT assertion' do
    let(:client) do
      OAuth2::Client.new(
        '',
        '',
        site: 'https://accounts.google.com',
        authorize_url: '/o/oauth2/auth',
        token_url: '/o/oauth2/token',
        auth_scheme: :request_body
      )
    end

    # These are taken directly from Google's documentation example:

    let(:required_claims) do
      {
        'iss' => '761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com',
        # The email address of the service account.

        'scope' => 'https://www.googleapis.com/auth/devstorage.readonly https://www.googleapis.com/auth/prediction',
        # A space-delimited list of the permissions that the application requests.

        'aud' => 'https://www.googleapis.com/oauth2/v4/token',
        # A descriptor of the intended target of the assertion. When making an access token request this value
        # is always https://www.googleapis.com/oauth2/v4/token.

        'exp' => Time.now.to_i + 3600,
        # The expiration time of the assertion, specified as seconds since 00:00:00 UTC, January 1, 1970. This value
        # has a maximum of 1 hour after the issued time.

        'iat' => Time.now.to_i,
        # The time the assertion was issued, specified as seconds since 00:00:00 UTC, January 1, 1970.
      }
    end

    let(:optional_claims) do
      {
        'sub' => 'some.user@example.com',
        # The email address of the user for which the application is requesting delegated access.
      }
    end

    let(:algorithm) { 'RS256' }
    # Per Google: "Service accounts rely on the RSA SHA-256 algorithm"

    let(:key) do
      begin
        OpenSSL::PKCS12.new(File.read('spec/fixtures/google_service_account_key.p12'), 'notasecret').key
        # This simulates the .p12 file that Google gives you to download and keep somewhere.  This is meant to
        # illustrate extracting the key and using it to generate the JWT.
      rescue OpenSSL::PKCS12::PKCS12Error
        # JRuby CI builds are blowing up trying to extract a sample key for some reason.  This simulates the end result
        # of actually figuring out the problem.
        OpenSSL::PKey::RSA.new(1024)
      end
    end
    # Per Google:

    # "Take note of the service account's email address and store the service account's P12 private key file in a
    # location accessible to your application. Your application needs them to make authorized API calls."

    let(:encoding_options) { {key: key, algorithm: algorithm} }

    before do
      client.connection = Faraday.new(client.site, client.options[:connection_opts]) do |builder|
        builder.request :url_encoded
        builder.adapter :test do |stub|
          stub.post('https://accounts.google.com/o/oauth2/token') do |token_request|
            @request_body = Rack::Utils.parse_nested_query(token_request.body).transform_keys(&:to_sym)

            [
              200,

              {
                'Content-Type' => 'application/json',
              },

              {
                'access_token' => '1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M',
                'token_type' => 'Bearer',
                'expires_in' => 3600,
              }.to_json,
            ]
          end
        end
      end
    end

    context 'when passing the required claims' do
      let(:claims) { required_claims }

      it 'sends a JWT with the 5 keys' do
        client.assertion.get_token(claims, encoding_options)

        expect(@request_body).not_to be_nil, 'No access token request was made!'
        expect(@request_body[:grant_type]).to eq('urn:ietf:params:oauth:grant-type:jwt-bearer')
        expect(@request_body[:assertion]).to be_a(String)

        payload, header = JWT.decode(@request_body[:assertion], key, true, algorithm: algorithm)
        expect(header['alg']).to eq('RS256')
        expect(payload.keys).to match_array(%w[iss scope aud exp iat])

        # Note that these specifically do _not_ include the 'sub' claim, which is indicated as being 'required'
        # by the OAuth2 JWT RFC: https://tools.ietf.org/html/rfc7523#section-3
        # This may indicate that this is a nonstandard use case by Google.

        payload.each do |key, value|
          expect(value).to eq(claims[key])
        end
      end
    end

    context 'when including the optional `sub` claim' do
      let(:claims) { required_claims.merge(optional_claims) }

      it 'sends a JWT with the 6 keys' do
        client.assertion.get_token(claims, encoding_options)

        expect(@request_body).not_to be_nil, 'No access token request was made!'
        expect(@request_body[:grant_type]).to eq('urn:ietf:params:oauth:grant-type:jwt-bearer')
        expect(@request_body[:assertion]).to be_a(String)

        payload, header = JWT.decode(@request_body[:assertion], key, true, algorithm: algorithm)
        expect(header['alg']).to eq('RS256')
        expect(payload.keys).to match_array(%w[iss scope aud exp iat sub])

        payload.each do |key, value|
          expect(value).to eq(claims[key])
        end
      end
    end
  end
end