google-sa-auth 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -0
- data/README.md +4 -0
- data/Rakefile +1 -0
- data/google-sa-auth.gemspec +25 -0
- data/lib/google-sa-auth.rb +88 -0
- data/lib/google-sa-auth/client.rb +43 -0
- data/lib/google-sa-auth/scope.rb +78 -0
- data/lib/google-sa-auth/token.rb +34 -0
- metadata +134 -0
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "google-sa-auth"
|
6
|
+
s.version = "0.0.5"
|
7
|
+
s.authors = ["Jon Durbin"]
|
8
|
+
s.email = ["jond@greenviewdata.com"]
|
9
|
+
s.homepage = "https://github.com/gdi/google-sa-auth"
|
10
|
+
s.summary = %q{Simple gem for generating authorization tokens for google service accounts}
|
11
|
+
s.description = %q{Simple gem for generating authorization tokens for google service accounts}
|
12
|
+
|
13
|
+
s.rubyforge_project = "google-sa-auth"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency 'rspec'
|
21
|
+
s.add_dependency 'bundler'
|
22
|
+
s.add_dependency 'curb-fu'
|
23
|
+
s.add_dependency 'google-jwt'
|
24
|
+
s.add_dependency 'json'
|
25
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'google-jwt'
|
2
|
+
require 'curb-fu'
|
3
|
+
require 'google-sa-auth/scope'
|
4
|
+
require 'google-sa-auth/token'
|
5
|
+
require 'google-sa-auth/client'
|
6
|
+
|
7
|
+
class GoogleSAAuth
|
8
|
+
attr_accessor :claim_set, :pkcs12, :token
|
9
|
+
def initialize(args)
|
10
|
+
# Symbolize keys.
|
11
|
+
args = args.inject({}){|item,(k,v)| item[k.to_sym] = v; item}
|
12
|
+
|
13
|
+
# Remove unknown keys and make sure we have all the required keys.
|
14
|
+
args.delete_if {|k,v| ![:email_address, :scope, :key, :password, :audience].include?(k)}
|
15
|
+
[:email_address, :scope, :key].each do |required|
|
16
|
+
raise RuntimeError, "Missing required argument key #{required}" unless args[required]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Setup default password and audience.
|
20
|
+
args[:password] ||= 'notasecret'
|
21
|
+
args[:audience] ||= 'https://accounts.google.com/o/oauth2/token'
|
22
|
+
|
23
|
+
# Create the claim set.
|
24
|
+
self.claim_set = {:iss => args[:email_address], :aud => args[:audience]}
|
25
|
+
|
26
|
+
# Determine the scope.
|
27
|
+
if args[:scope].class == String
|
28
|
+
if args[:scope] =~ /^http/i
|
29
|
+
self.claim_set[:scope] = args[:scope]
|
30
|
+
else
|
31
|
+
self.claim_set[:scope] = GoogleSAAuth::Scope.new(args[:scope]).url
|
32
|
+
end
|
33
|
+
elsif args[:scope].class == Array
|
34
|
+
scopes = args[:scope].each.collect do |scope|
|
35
|
+
if scope =~ /http/i
|
36
|
+
scope
|
37
|
+
else
|
38
|
+
GoogleSAAuth::Scope.new(scope).url
|
39
|
+
end
|
40
|
+
end
|
41
|
+
self.claim_set[:scope] = scopes.join(' ')
|
42
|
+
elsif args[:scope].class == Hash
|
43
|
+
# Specify extension, e.g. => {:fusiontables => 'readonly'}
|
44
|
+
scopes = []
|
45
|
+
args[:scope].each do |scope,extension|
|
46
|
+
if scope =~ /^http/i
|
47
|
+
url = scope
|
48
|
+
else
|
49
|
+
url = GoogleSAAuth::Scope.new(scope).by_extension(extension)
|
50
|
+
end
|
51
|
+
scopes.push(url) unless url.nil?
|
52
|
+
end
|
53
|
+
self.claim_set[:scope] = scopes.join(' ')
|
54
|
+
end
|
55
|
+
|
56
|
+
# Set other attributes.
|
57
|
+
self.pkcs12 = {:key => args[:key], :password => args[:password]}
|
58
|
+
|
59
|
+
# Get our authorization.
|
60
|
+
auth_token
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def auth_token
|
65
|
+
# Make sure this token isn't expired.
|
66
|
+
self.token = nil if self.token.nil? || self.token.expired?
|
67
|
+
self.token ||= GoogleSAAuth::Token.new(jwt)
|
68
|
+
self.token
|
69
|
+
end
|
70
|
+
|
71
|
+
def token_string
|
72
|
+
# Return only the string for the token.
|
73
|
+
auth_token.token
|
74
|
+
end
|
75
|
+
|
76
|
+
def jwt
|
77
|
+
# Make sure the jwt isn't already expired.
|
78
|
+
@json_web_token = nil if @json_web_token.nil? || Time.now.to_i >= @json_web_token.claim_set[:exp]
|
79
|
+
|
80
|
+
# (Re)create the JSON web token.
|
81
|
+
@json_web_token ||= GoogleJWT.new(
|
82
|
+
self.claim_set,
|
83
|
+
self.pkcs12[:key],
|
84
|
+
self.pkcs12[:password]
|
85
|
+
)
|
86
|
+
@json_web_token
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class GoogleSAAuth
|
2
|
+
class Client
|
3
|
+
class << self
|
4
|
+
def run(args)
|
5
|
+
# Clean up our arguments.
|
6
|
+
args = args.inject({}){|item,(k,v)| item[k.to_sym] = v; item}
|
7
|
+
args = args.delete_if {|k,v| ![:uri, :data, :headers, :method].include?(k)}
|
8
|
+
args[:data] ||= {}
|
9
|
+
args[:headers] ||= {}
|
10
|
+
|
11
|
+
# Parse the uri.
|
12
|
+
protocol, host, path = parse_uri(args[:uri])
|
13
|
+
|
14
|
+
# Set up a nice host info hash.
|
15
|
+
host_info = {
|
16
|
+
:host => host,
|
17
|
+
:path => path,
|
18
|
+
:protocol => protocol,
|
19
|
+
:headers => args[:headers]
|
20
|
+
}
|
21
|
+
|
22
|
+
# Perform the request.
|
23
|
+
if args[:method] == 'get'
|
24
|
+
CurbFu.get(host_info, args[:data])
|
25
|
+
elsif args[:method] == 'post'
|
26
|
+
CurbFu.post(host_info, args[:data])
|
27
|
+
elsif args[:method] == 'put'
|
28
|
+
CurbFu.put(host_info, args[:data])
|
29
|
+
elsif args[:method] == 'delete'
|
30
|
+
CurbFu.delete(host_info, args[:data])
|
31
|
+
else
|
32
|
+
raise 'InvalidRequestType', "Unknown method: #{method}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def parse_uri(uri)
|
38
|
+
return [$1, $2, $3] if uri =~ /^(https?):\/\/([a-z0-9\.\-]+)(\/.*)$/i
|
39
|
+
raise ArgumentError "Error parsing URI: #{uri}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class GoogleSAAuth
|
2
|
+
class Scope
|
3
|
+
$KNOWN_APIS = {}
|
4
|
+
|
5
|
+
attr_accessor :scope_urls, :api_info
|
6
|
+
def initialize(name)
|
7
|
+
get_api_info(name.to_s)
|
8
|
+
end
|
9
|
+
|
10
|
+
def url
|
11
|
+
full_access
|
12
|
+
end
|
13
|
+
|
14
|
+
def full_access
|
15
|
+
# Get the shortest scope, which theoretically will be full permissions.
|
16
|
+
self.scope_urls.sort_by {|url| url.length}.first
|
17
|
+
end
|
18
|
+
|
19
|
+
def read_only
|
20
|
+
# Simply wrapper for .readonly permissions.
|
21
|
+
by_extension('readonly')
|
22
|
+
end
|
23
|
+
|
24
|
+
def by_extension(extension)
|
25
|
+
# If extension is nil, then we can just return full access.
|
26
|
+
return full_access if extension.nil?
|
27
|
+
|
28
|
+
# Try to find a scope by extension, e.g. ".readonly"
|
29
|
+
self.scope_urls.each do |url|
|
30
|
+
return url if url =~ /\.#{extension}$/i
|
31
|
+
end
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
def known_apis
|
37
|
+
return $KNOWN_APIS unless $KNOWN_APIS.empty?
|
38
|
+
|
39
|
+
# Get the list of known APIs using the Google's API Discovery
|
40
|
+
response = GoogleSAAuth::Client.run(
|
41
|
+
:uri => 'https://www.googleapis.com/discovery/v1/apis',
|
42
|
+
:method => 'get'
|
43
|
+
)
|
44
|
+
|
45
|
+
# Make sure we got a 200 response.
|
46
|
+
raise RuntimeError unless response.status == 200
|
47
|
+
|
48
|
+
# Parse the json and store the known apis.
|
49
|
+
result = JSON.parse(response.body)
|
50
|
+
apis = {}
|
51
|
+
result['items'].each {|item| apis[item['name']] = item}
|
52
|
+
$KNOWN_APIS = apis
|
53
|
+
apis
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_api_info(scope_name)
|
57
|
+
return if self.scope_urls
|
58
|
+
|
59
|
+
# Make sure google listed this API.
|
60
|
+
api_info = known_apis[scope_name]
|
61
|
+
return nil unless api_info && api_info['discoveryRestUrl']
|
62
|
+
|
63
|
+
# Get the OAuth 2.0 scope url.
|
64
|
+
response = GoogleSAAuth::Client.run(
|
65
|
+
:uri => api_info['discoveryRestUrl'],
|
66
|
+
:method => 'get'
|
67
|
+
)
|
68
|
+
|
69
|
+
# Make sure we got a 200 response.
|
70
|
+
raise RuntimeError unless response.status == 200
|
71
|
+
|
72
|
+
# Parse the result and try to determine scope URLs.
|
73
|
+
result = JSON.parse(response.body)
|
74
|
+
self.scope_urls = result['auth']['oauth2']['scopes'].keys rescue nil
|
75
|
+
self.api_info = result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class GoogleSAAuth
|
2
|
+
class Token
|
3
|
+
attr_accessor :token, :response, :expires_at
|
4
|
+
def initialize(jwt)
|
5
|
+
# Get the token upon initialization and symbolize the keys.
|
6
|
+
self.response = get_auth_token(jwt.jwt).inject({}){|item,(k,v)| item[k.to_sym] = v; item}
|
7
|
+
self.token = self.response[:access_token]
|
8
|
+
self.expires_at = jwt.claim_set[:exp]
|
9
|
+
end
|
10
|
+
|
11
|
+
def expired?
|
12
|
+
Time.now.to_i >= self.expires_at
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def get_auth_token(jwt)
|
17
|
+
# Post to the google oauth2 token URL.
|
18
|
+
result = GoogleSAAuth::Client.run(
|
19
|
+
:uri => 'https://accounts.google.com/o/oauth2/token',
|
20
|
+
:headers => {
|
21
|
+
'Content-Type' => 'application/x-www-form-urlencoded'
|
22
|
+
},
|
23
|
+
:data => "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=#{jwt}",
|
24
|
+
:method => 'post'
|
25
|
+
)
|
26
|
+
|
27
|
+
# Throw an exception unless we got a 200 response.
|
28
|
+
raise RuntimeError, "Error getting authentication token: #{result.body}" unless result.status == 200
|
29
|
+
|
30
|
+
# Parse the results.
|
31
|
+
JSON.parse(result.body.to_s)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: google-sa-auth
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jon Durbin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-02 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: bundler
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: curb-fu
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: google-jwt
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: json
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: Simple gem for generating authorization tokens for google service accounts
|
95
|
+
email:
|
96
|
+
- jond@greenviewdata.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- Gemfile
|
102
|
+
- README.md
|
103
|
+
- Rakefile
|
104
|
+
- google-sa-auth.gemspec
|
105
|
+
- lib/google-sa-auth.rb
|
106
|
+
- lib/google-sa-auth/client.rb
|
107
|
+
- lib/google-sa-auth/scope.rb
|
108
|
+
- lib/google-sa-auth/token.rb
|
109
|
+
homepage: https://github.com/gdi/google-sa-auth
|
110
|
+
licenses: []
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ! '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project: google-sa-auth
|
129
|
+
rubygems_version: 1.8.24
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: Simple gem for generating authorization tokens for google service accounts
|
133
|
+
test_files: []
|
134
|
+
has_rdoc:
|