From aeef8f3a5cfabad1edf4619e325feef811742ff6 Mon Sep 17 00:00:00 2001 From: Steven Bazyl Date: Wed, 10 Oct 2012 14:18:33 -0600 Subject: [PATCH] Re-org service account support --- lib/google/api_client/auth/jwt_asserter.rb | 124 ++++++++++++++++++ lib/google/api_client/auth/pkcs12.rb | 48 +++++++ lib/google/api_client/service_account.rb | 145 +-------------------- 3 files changed, 174 insertions(+), 143 deletions(-) create mode 100644 lib/google/api_client/auth/jwt_asserter.rb create mode 100644 lib/google/api_client/auth/pkcs12.rb diff --git a/lib/google/api_client/auth/jwt_asserter.rb b/lib/google/api_client/auth/jwt_asserter.rb new file mode 100644 index 000000000..62cfd8689 --- /dev/null +++ b/lib/google/api_client/auth/jwt_asserter.rb @@ -0,0 +1,124 @@ +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'jwt' +require 'signet/oauth_2/client' + +module Google + class APIClient + ## + # Generates access tokens using the JWT assertion profile. Requires a + # service account & access to the private key. + # + # @example + # + # client = Google::APIClient.new + # key = Google::APIClient::PKCS12.load_key('client.p12', 'notasecret') + # service_account = Google::APIClient::JWTAsserter( + # '123456-abcdef@developer.gserviceaccount.com', + # 'https://www.googleapis.com/auth/prediction', + # key) + # client.authorization = service_account.authorize + # client.execute(...) + # + # @see https://developers.google.com/accounts/docs/OAuth2ServiceAccount + class JWTAsserter + # @return [String] ID/email of the issuing party + attr_accessor :issuer + # @return [Fixnum] How long, in seconds, the assertion is valid for + attr_accessor :expiry + # @return [Fixnum] Seconds to expand the issued at/expiry window to account for clock skew + attr_accessor :skew + # @return [String] Scopes to authorize + attr_reader :scope + # @return [OpenSSL::PKey] key for signing assertions + attr_writer :key + + ## + # Initializes the asserter for a service account. + # + # @param [String] issuer + # Name/ID of the client issuing the assertion + # @param [String, Array] scope + # Scopes to authorize. May be a space delimited string or array of strings + # @param [OpenSSL::PKey] key + # RSA private key for signing assertions + def initialize(issuer, scope, key) + self.issuer = issuer + self.scope = scope + self.expiry = 60 # 1 min default + self.skew = 60 + self.key = key + end + + ## + # Set the scopes to authorize + # + # @param [String, Array] new_scope + # Scopes to authorize. May be a space delimited string or array of strings + def scope=(new_scope) + case new_scope + when Array + @scope = new_scope.join(' ') + when String + @scope = new_scope + when nil + @scope = '' + else + raise TypeError, "Expected Array or String, got #{new_scope.class}" + end + end + + ## + # Builds & signs the assertion. + # + # @param [String] person + # Email address of a user, if requesting a token to act on their behalf + # @return [String] Encoded JWT + def to_jwt(person=nil) + now = Time.new + assertion = { + "iss" => @issuer, + "scope" => self.scope, + "aud" => "https://accounts.google.com/o/oauth2/token", + "exp" => (now + expiry).to_i, + "iat" => (now - skew).to_i + } + assertion['prn'] = person unless person.nil? + return JWT.encode(assertion, @key, "RS256") + end + + ## + # Request a new access token. + # + # @param [String] person + # Email address of a user, if requesting a token to act on their behalf + # @param [Hash] options + # Pass through to Signet::OAuth2::Client.fetch_access_token + # @return [Signet::OAuth2::Client] Access token + # + # @see Signet::OAuth2::Client.fetch_access_token + def authorize(person = nil, options={}) + assertion = self.to_jwt(person) + authorization = Signet::OAuth2::Client.new( + :token_credential_uri => 'https://accounts.google.com/o/oauth2/token' + ) + authorization.grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer' + authorization.extension_parameters = { :assertion => assertion } + authorization.fetch_access_token!(options) + return authorization + end + end + end +end diff --git a/lib/google/api_client/auth/pkcs12.rb b/lib/google/api_client/auth/pkcs12.rb new file mode 100644 index 000000000..84bcda54e --- /dev/null +++ b/lib/google/api_client/auth/pkcs12.rb @@ -0,0 +1,48 @@ +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Google + class APIClient + ## + # Helper for loading keys from the PKCS12 files downloaded when + # setting up service accounts at the APIs Console. + # + module PKCS12 + ## + # Loads a key from PKCS12 file, assuming a single private key + # is present. + # + # @param [String] keyfile + # Path of the PKCS12 file to load. If not a path to an actual file, + # assumes the string is the content of the file itself. + # @param [String] passphrase + # Passphrase for unlocking the private key + # + # @return [OpenSSL::PKey] The private key for signing assertions. + def self.load_key(keyfile, passphrase) + begin + if File.exists?(keyfile) + content = File.read(keyfile) + else + content = keyfile + end + pkcs12 = OpenSSL::PKCS12.new(content, passphrase) + return pkcs12.key + rescue OpenSSL::PKCS12::PKCS12Error + raise ArgumentError.new("Invalid keyfile or passphrase") + end + end + end + end +end diff --git a/lib/google/api_client/service_account.rb b/lib/google/api_client/service_account.rb index 54737add3..690bd054e 100644 --- a/lib/google/api_client/service_account.rb +++ b/lib/google/api_client/service_account.rb @@ -12,146 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'jwt' -require 'signet/oauth_2/client' - -module Google - class APIClient - ## - # Helper for loading keys from the PKCS12 files downloaded when - # setting up service accounts at the APIs Console. - # - - module PKCS12 - - ## - # Loads a key from PKCS12 file, assuming a single private key - # is present. - # - # @param [String] keyfile - # Path of the PKCS12 file to load. If not a path to an actual file, - # assumes the string is the content of the file itself. - # @param [String] passphrase - # Passphrase for unlocking the private key - # - # @return [OpenSSL::PKey] The private key for signing assertions. - def self.load_key(keyfile, passphrase) - begin - if File.exists?(keyfile) - content = File.read(keyfile) - else - content = keyfile - end - pkcs12 = OpenSSL::PKCS12.new(content, passphrase) - return pkcs12.key - rescue OpenSSL::PKCS12::PKCS12Error - raise ArgumentError.new("Invalid keyfile or passphrase") - end - end - end - - ## - # Generates access tokens using the JWT assertion profile. Requires a - # service account & access to the private key. - # - # @example - # - # client = Google::APIClient.new - # key = Google::APIClient::PKCS12.load_key('client.p12', 'notasecret') - # service_account = Google::APIClient::JWTAsserter( - # '123456-abcdef@developer.gserviceaccount.com', - # 'https://www.googleapis.com/auth/prediction', - # key) - # client.authorization = service_account.authorize - # client.execute(...) - # - # @see https://developers.google.com/accounts/docs/OAuth2ServiceAccount - class JWTAsserter - # @return [String] ID/email of the issuing party - attr_accessor :issuer - # @return [Fixnum] How long, in seconds, the assertion is valid for - attr_accessor :expiry - # @return [Fixnum] Seconds to expand the issued at/expiry window to account for clock skew - attr_accessor :skew - # @return [String] Scopes to authorize - attr_reader :scope - # @return [OpenSSL::PKey] key for signing assertions - attr_writer :key - - ## - # Initializes the asserter for a service account. - # - # @param [String] issuer - # Name/ID of the client issuing the assertion - # @param [String, Array] scope - # Scopes to authorize. May be a space delimited string or array of strings - # @param [OpenSSL::PKey] key - # RSA private key for signing assertions - def initialize(issuer, scope, key) - self.issuer = issuer - self.scope = scope - self.expiry = 60 # 1 min default - self.skew = 60 - self.key = key - end - - ## - # Set the scopes to authorize - # - # @param [String, Array] new_scope - # Scopes to authorize. May be a space delimited string or array of strings - def scope=(new_scope) - case new_scope - when Array - @scope = new_scope.join(' ') - when String - @scope = new_scope - when nil - @scope = '' - else - raise TypeError, "Expected Array or String, got #{new_scope.class}" - end - end - - ## - # Builds & signs the assertion. - # - # @param [String] person - # Email address of a user, if requesting a token to act on their behalf - # @return [String] Encoded JWT - def to_jwt(person=nil) - now = Time.new - assertion = { - "iss" => @issuer, - "scope" => self.scope, - "aud" => "https://accounts.google.com/o/oauth2/token", - "exp" => (now + expiry).to_i, - "iat" => (now - skew).to_i - } - assertion['prn'] = person unless person.nil? - return JWT.encode(assertion, @key, "RS256") - end - - ## - # Request a new access token. - # - # @param [String] person - # Email address of a user, if requesting a token to act on their behalf - # @param [Hash] options - # Pass through to Signet::OAuth2::Client.fetch_access_token - # @return [Signet::OAuth2::Client] Access token - # - # @see Signet::OAuth2::Client.fetch_access_token - def authorize(person = nil, options={}) - assertion = self.to_jwt(person) - authorization = Signet::OAuth2::Client.new( - :token_credential_uri => 'https://accounts.google.com/o/oauth2/token' - ) - authorization.grant_type = 'urn:ietf:params:oauth:grant-type:jwt-bearer' - authorization.extension_parameters = { :assertion => assertion } - authorization.fetch_access_token!(options) - return authorization - end - end - end -end +require 'google/api_client/auth/pkcs12' +require 'google/api_client/auth/jwt_asserter'