Last active
February 28, 2018 09:13
-
-
Save bjeanes/e72e81b9c2582c91d772def4fe07810f to your computer and use it in GitHub Desktop.
Just a set of POROs for emulating a simple OAuth2 provider
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
require 'securerandom' | |
module Oauth | |
class StubProvider | |
AUTHORIZATION_TTL = 60.seconds | |
ACCESS_TTL = 1.hour | |
def initialize(authorization_ttl: AUTHORIZATION_TTL, access_ttl: ACCESS_TTL, **meta) | |
@authorization_ttl = authorization_ttl | |
@access_ttl = access_ttl | |
@tokens = [] | |
end | |
private def tokens | |
@tokens.reject! { |t| t.expired? } | |
@tokens | |
end | |
class Token | |
attr_reader :expiry | |
def initialize(authorization_ttl:, access_ttl:, **meta) | |
@authorization_code = SecureRandom.uuid | |
@meta = meta | |
@expiry = Time.now + authorization_ttl | |
@access_ttl = access_ttl | |
end | |
def authorization_code | |
@authorization_code | |
end | |
def authorize!(authorization_code) | |
if authorization_code.present? && !expired? && @authorization_code == authorization_code | |
set_tokens | |
else | |
false | |
end | |
end | |
def info(access_token) | |
if access_token.present? && !expired? && @access_token == access_token | |
@meta | |
else | |
false | |
end | |
end | |
def refresh!(refresh_token) | |
if refresh_token.present? && !expired? && @refresh_token == refresh_token | |
set_tokens | |
else | |
false | |
end | |
end | |
private def set_tokens | |
@authorization_code = nil | |
@access_token = SecureRandom.uuid | |
@refresh_token = SecureRandom.uuid | |
@expiry = Time.now + @access_ttl | |
{ | |
access_token: @access_token, | |
refresh_token: @refresh_token, | |
expiry: expiry | |
} | |
end | |
def expired? | |
Time.now > expiry | |
end | |
end | |
def generate_authorization(**meta) | |
token = Token.new(**meta, authorization_ttl: @authorization_ttl, access_ttl: @access_ttl) | |
tokens << token | |
{ | |
authorization_code: token.authorization_code, | |
expiry: token.expiry, | |
} | |
end | |
def authorize(code) | |
tokens.each { |t| return (t.authorize!(code) || next) } | |
false | |
end | |
def refresh(refresh_token) | |
tokens.each { |t| return (t.refresh!(refresh_token) || next) } | |
false | |
end | |
def info(access_token) | |
tokens.each { |t| return (t.info(access_token) || next) } | |
false | |
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
require 'spec_helper' | |
RSpec.describe Oauth::StubProvider do | |
let(:provider) { described_class.new } | |
it 'generates an authorization token' do | |
expect(provider.generate_authorization).to match hash_including({ | |
expiry: instance_of(Time), | |
authorization_code: /.+/, | |
}) | |
end | |
it 'does not authorize expired authorization tokens' do | |
data = provider.generate_authorization | |
Timecop.travel(data[:expiry] + 1.second) do | |
expect(provider.authorize(data[:authorization_code])).to eq false | |
end | |
end | |
it 'does not authorize invalid authorization tokens' do | |
provider.generate_authorization | |
expect(provider.authorize(SecureRandom.uuid)).to eq false | |
end | |
it 'authorizes valid authorization token exactly once' do | |
data = provider.generate_authorization | |
expect(provider.authorize(data[:authorization_code])).to match hash_including({ | |
expiry: instance_of(Time), | |
access_token: /.+/, | |
refresh_token: /.+/, | |
}) | |
expect(provider.authorize(data[:authorization_code])).to eq false | |
end | |
it 'does not give access with an authorization token' do | |
auth = provider.generate_authorization[:authorization_code] | |
expect(provider.info(auth)).to eq false | |
end | |
it 'does not refresh with an authorization token' do | |
auth = provider.generate_authorization[:authorization_code] | |
expect(provider.refresh(auth)).to eq false | |
end | |
it 'does not refresh with an expired refresh token' do | |
auth = provider.generate_authorization[:authorization_code] | |
data = provider.authorize(auth) | |
Timecop.travel(data[:expiry] + 1.second) do | |
expect(provider.refresh(data[:refresh_token])).to eq false | |
end | |
end | |
it 'does not refresh with an access token' do | |
auth = provider.generate_authorization[:authorization_code] | |
data = provider.authorize(auth) | |
expect(provider.refresh(data[:access_token])).to eq false | |
end | |
it 'refreshes tokens with valid refresh token and invalidates previous tokens' do | |
auth = provider.generate_authorization[:authorization_code] | |
data = provider.authorize(auth) | |
refreshed_data = provider.refresh(data[:refresh_token]) | |
expect(refreshed_data).to match hash_including({ | |
expiry: instance_of(Time), | |
access_token: /.+/, | |
refresh_token: /.+/, | |
}) | |
expect(refreshed_data[:expiry]).to be > data[:expiry] | |
expect(refreshed_data[:access_token]).to_not eq data[:access_token] | |
expect(refreshed_data[:access_token]).to_not eq data[:access_token] | |
expect(provider.info(data[:access_token])).to eq false | |
expect(provider.refresh(data[:refresh_token])).to eq false | |
expect(provider.info(refreshed_data[:access_token])).to_not eq false | |
end | |
it 'does not return token info when using an expired access token' do | |
auth = provider.generate_authorization[:authorization_code] | |
data = provider.authorize(auth) | |
Timecop.travel(data[:expiry] + 1.second) do | |
expect(provider.info(data[:access_token])).to eq false | |
end | |
end | |
it 'extends expiry when refreshing' do | |
auth = provider.generate_authorization[:authorization_code] | |
data = provider.authorize(auth) | |
Timecop.travel(data[:expiry] - 1.second) do | |
refreshed_data = provider.refresh(data[:refresh_token]) | |
expect(refreshed_data[:expiry]).to be > data[:expiry] | |
end | |
end | |
it 'does not return token info when using an invalid access token' do | |
auth = provider.generate_authorization[:authorization_code] | |
token = provider.authorize(auth)[:access_token] | |
expect(provider.info(SecureRandom.uuid)).to eq false | |
end | |
it 'returns token info associated with initial grant' do | |
info = { data: Object.new } | |
auth = provider.generate_authorization(info)[:authorization_code] | |
token = provider.authorize(auth)[:access_token] | |
expect(provider.info(token)).to eql info | |
end | |
it 'allows configuring the expiry TTLs' do | |
authorization_ttl = 1.second | |
access_ttl = 10.seconds | |
provider = described_class.new(access_ttl: access_ttl, authorization_ttl: authorization_ttl) | |
auth = data = nil | |
Timecop.freeze do | |
auth = provider.generate_authorization | |
expect(auth[:expiry]).to eq(Time.now + authorization_ttl) | |
end | |
Timecop.freeze do | |
data = provider.authorize(auth[:authorization_code]) | |
expect(data[:expiry]).to eq(Time.now + access_ttl) | |
end | |
Timecop.freeze do | |
data = provider.refresh(data[:refresh_token]) | |
expect(data[:expiry]).to eq(Time.now + access_ttl) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment