-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[TOOLSLIBS-1753] Adds Oauth support (#120)
* adds token fetcher * oauth and client changes * oauth and client changes * added key verify + exception * adds tests, docs * adds test, fixes typo * fixes comment * fixes comment * restricts bundler version * github workflow change * github workflow change * github workflow change * changed variable name * Nate's suggestions * fixes tests * added retry * docs change * adds power to retry --------- Co-authored-by: Jade Westover <[email protected]>
- Loading branch information
Showing
9 changed files
with
260 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
require 'urbanairship' | ||
require 'base64' | ||
require 'rest-client' | ||
|
||
module Urbanairship | ||
class Oauth | ||
attr_accessor :client_id, :sub, :assertion_private_key, :ip_addresses, :scopes, :oauth_server | ||
|
||
# Initialize Oauth class | ||
# | ||
# @param [String] client_id The Client ID found when creating Oauth credentials in the dashboard. | ||
# @param [String] key The app key for the project. | ||
# @param [String] assertion_private_key The private key found when creating Oauth credentials in the dashboard. Used for assertion token auth. | ||
# @param [Array<String>] ip_addresses A list of CIDR representations of valid IP addresses to which the issued token is restricted. Example: ['24.20.40.0/22', '34.17.3.0/22'] | ||
# @param [Array<String>] scopes A list of scopes to which the issued token will be entitled. Example: ['psh', 'lst'] | ||
# @param [String] oauth_server The server to send Oauth token requests to. By default is 'oauth2.asnapius.com', but can be set to 'oauth2.asnapieu.com' if using the EU server. | ||
# @return [Object] Oauth object | ||
def initialize(client_id:, key:, assertion_private_key:, ip_addresses: [], scopes: [], oauth_server: Urbanairship.configuration.oauth_server) | ||
@grant_type = 'client_credentials' | ||
@client_id = client_id | ||
@assertion_private_key = assertion_private_key | ||
@ip_addresses = ip_addresses | ||
@scopes = scopes | ||
@sub = "app:#{key}" | ||
@oauth_server = oauth_server | ||
@token = nil | ||
end | ||
|
||
# Get an Oauth token from Airship Oauth servers. | ||
# | ||
# @return [String] JSON web token to be used in further Airship API requests. | ||
def get_token | ||
unless @token.nil? | ||
decoded_jwt = JWT.decode(@token, nil, false) | ||
current_time = Time.now.to_i | ||
expiry_time = decoded_jwt[0]['exp'] | ||
|
||
if current_time < expiry_time | ||
return @token | ||
end | ||
end | ||
|
||
assertion_jwt = build_assertion_jwt | ||
|
||
url = "https://#{@oauth_server}/token" | ||
headers = { | ||
'Host': @oauth_server, | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
'Accept': 'application/json' | ||
} | ||
|
||
params = { | ||
method: :post, | ||
url: url, | ||
headers: headers, | ||
payload: { | ||
grant_type: @grant_type, | ||
assertion: assertion_jwt | ||
}, | ||
timeout: 60 | ||
} | ||
|
||
retries = 0 | ||
max_retries = 3 | ||
begin | ||
response = RestClient::Request.execute(params) | ||
@token = JSON.parse(response.body)['access_token'] | ||
return @token | ||
rescue RestClient::ExceptionWithResponse => e | ||
if [400, 401, 406].include?(e.response.code) | ||
raise e | ||
else | ||
retries += 1 | ||
if retries <= max_retries | ||
sleep(retries ** 2) | ||
retry | ||
else | ||
new_error = RestClient::Exception.new(e.response, e.response.code) | ||
new_error.message = "failed after 3 attempts with error: #{e}" | ||
raise new_error | ||
end | ||
end | ||
end | ||
end | ||
|
||
# Build an assertion JWT | ||
# | ||
# @return [String] Assertion JWT to be used when requesting an Oauth token from Airship servers. | ||
def build_assertion_jwt | ||
assertion_expiration = 61 | ||
private_key = OpenSSL::PKey::EC.new(@assertion_private_key) | ||
|
||
headers = { | ||
alg: 'ES384', | ||
kid: @client_id | ||
} | ||
|
||
claims = { | ||
aud: "https://#{@oauth_server}/token", | ||
exp: Time.now.to_i + assertion_expiration, | ||
iat: Time.now.to_i, | ||
iss: @client_id, | ||
nonce: SecureRandom.uuid, | ||
sub: @sub | ||
} | ||
|
||
claims[:scope] = @scopes.join(' ') if @scopes.any? | ||
claims[:ipaddr] = @ip_addresses.join(' ') if @ip_addresses.any? | ||
|
||
JWT.encode(claims, private_key, 'ES384', headers) | ||
end | ||
|
||
# Verify a public key | ||
# | ||
# @param [String] key_id The key ID ('kid') found in the header when decoding an Oauth token granted from Airship's servers. | ||
# @return [String] The public key associated with the Key ID. | ||
def verify_public_key(key_id) | ||
url = "https://#{@oauth_server}/verify/public_key/#{key_id}" | ||
|
||
headers = { | ||
'Host': @oauth_server, | ||
'Accept': 'text/plain' | ||
} | ||
|
||
response = RestClient.get(url, headers) | ||
response.body | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
require 'spec_helper' | ||
require 'urbanairship/oauth' | ||
|
||
describe Urbanairship::Oauth do | ||
UA = Urbanairship | ||
|
||
it 'is instantiated with Oauth assertion auth' do | ||
oauth = UA::Oauth.new( | ||
client_id: 'hf73hfh_test_client_id_83hrg', | ||
assertion_private_key: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----', | ||
key: '37djhf_test_app_key_ndf8h3' | ||
) | ||
expect(oauth).not_to be_nil | ||
end | ||
|
||
it 'requests a token using assertion auth' do | ||
assertion_jwt = 'test_assertion' | ||
private_key = '-----BEGIN PRIVATE KEY----- | ||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAyfZQIiXQwXabABKqV | ||
LWU/Yek+jz/OIdEMK4nvaa77/nNTc6WgzudKityW09PuJIKhZANiAATaKO7pdTRk | ||
NDqMIFjtTILog5pfX+OZkrMr+2i3VoQoiFwzJO0fh0xCJ2Lg1l7nYIOCs09/deb1 | ||
fwMOSxoXG/IMD3AqqwqZzRmgeKfnupueqO3RNxngJUL+0zQTW+dSXWk= | ||
-----END PRIVATE KEY----- | ||
' | ||
|
||
oauth = UA::Oauth.new(client_id: 'test123', key: 'testappkey', assertion_private_key: private_key) | ||
|
||
request_params = { | ||
payload: { | ||
assertion: assertion_jwt, | ||
grant_type: "client_credentials" | ||
} | ||
} | ||
|
||
allow(oauth).to receive(:build_assertion_jwt).and_return(assertion_jwt) | ||
|
||
mock_response = double('response') | ||
allow(mock_response).to(receive_messages(code: 200, headers: '', body: '{"access_token": "mock_token"}')) | ||
expect(RestClient::Request).to(receive(:execute).with(hash_including(request_params))).and_return(mock_response) | ||
|
||
token = oauth.get_token | ||
expect(token).to eq("mock_token") | ||
end | ||
|
||
it 'builds an assertion jwt' do | ||
# This is a private key from revoked oauth credentials on a test app. | ||
# An actual private key is required to test this properly. | ||
private_key = '-----BEGIN PRIVATE KEY----- | ||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAyfZQIiXQwXabABKqV | ||
LWU/Yek+jz/OIdEMK4nvaa77/nNTc6WgzudKityW09PuJIKhZANiAATaKO7pdTRk | ||
NDqMIFjtTILog5pfX+OZkrMr+2i3VoQoiFwzJO0fh0xCJ2Lg1l7nYIOCs09/deb1 | ||
fwMOSxoXG/IMD3AqqwqZzRmgeKfnupueqO3RNxngJUL+0zQTW+dSXWk= | ||
-----END PRIVATE KEY----- | ||
' | ||
|
||
oauth = UA::Oauth.new(client_id: 'test123', key: 'testappkey', assertion_private_key: private_key) | ||
assertion_jwt = oauth.build_assertion_jwt | ||
|
||
expect(assertion_jwt).not_to be_nil | ||
end | ||
|
||
it 'should retrieve a public key' do | ||
oauth = UA::Oauth.new(client_id: 'test123', key: 'testappkey', assertion_private_key: 'testsecret') | ||
key_id = 'test123' | ||
public_key = 'test_public_key' | ||
server = 'https://oauth2.asnapius.com/verify/public_key/test123' | ||
|
||
mock_response = double('response') | ||
allow(mock_response).to(receive_messages(code: 200, headers: '', body: public_key)) | ||
expect(RestClient).to(receive(:get).with(server, {'Host': oauth.oauth_server, 'Accept': 'text/plain'})) | ||
.and_return(mock_response) | ||
|
||
public_key = oauth.verify_public_key(key_id) | ||
expect(public_key).to eq("test_public_key") | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters