# frozen_string_literal: true
require "json"
require "base32"
require "base64"
require "openssl"
require "securerandom"
# A library to implement a MagentaSSO provider or client.
module MagentaSSO
# The base MagentaSSO error.
class MagentaError < StandardError; end
# Raised by verification methods when a signature is incorrect.
class SignatureError < MagentaError; end
class << self
# Encode the +payload+ and generate a signature with the +secret+.
def encode_and_sign(payload, secret)
secret = Base32.decode(secret)
payload = JSON.generate(payload)
payload = Base64.urlsafe_encode64(payload, padding: false)
signature = OpenSSL::HMAC.digest("SHA256", secret, payload)
signature = Base64.urlsafe_encode64(signature, padding: false)
[payload, signature]
end
# Verify the +signature+ using the +secret+, and return the decoded +payload+.
def verify_and_decode(payload, signature, secret)
secret = Base32.decode(secret)
signature = Base64.urlsafe_decode64(signature)
our_signature = OpenSSL::HMAC.digest("SHA256", secret, payload)
raise MagentaSSO::SignatureError unless signature == our_signature
payload = Base64.urlsafe_decode64(payload)
JSON.parse(payload)
end
end
# A Magenta authentication request.
class Request
attr_accessor :client_id, :client_secret, :nonce, :scopes, :callback_url
def initialize(client_id, client_secret, nonce, scopes, callback_url)
@client_id = client_id
@client_secret = client_secret
@nonce = nonce || SecureRandom.random_number(9_999_999)
@scopes = [scopes].flatten
@callback_url = callback_url
end
def self.verify(payload, signature, client_secret)
payload = ::MagentaSSO.verify_and_decode(payload, signature, client_secret)
new(
payload["client_id"],
client_secret,
payload["nonce"],
payload["scopes"],
payload["callback_url"]
)
end
def sign
payload = {
client_id: @client_id,
nonce: @nonce,
scopes: @scopes,
callback_url: @callback_url
}
::MagentaSSO.encode_and_sign(payload, @client_secret)
end
def query_params
payload, signature = sign
{
client: @client_id,
payload: payload,
signature: signature
}
end
end
# A Magenta authentication response
class Response
attr_accessor :client_id, :client_secret, :nonce, :user_data, :scope_data
def initialize(client_id, client_secret, nonce, user_data, scope_data)
@client_id = client_id
@client_secret = client_secret
@nonce = nonce
@user_data = user_data
@scope_data = scope_data
end
def self.verify(payload, signature, client_secret)
payload = ::MagentaSSO.verify_and_decode(payload, signature, client_secret)
new(
payload["client_id"],
client_secret,
payload["nonce"],
payload["user_data"],
payload["scope_data"]
)
end
def sign
payload = {
client_id: @client_id,
nonce: @nonce,
user_data: @user_data,
scope_data: @scope_data
}
::MagentaSSO.encode_and_sign(payload, @client_secret)
end
def query_params
payload, signature = sign
{
payload: payload,
signature: signature
}
end
def external_id
@user_data&.[]("external_id")
end
def email_address
@user_data&.[]("email")
end
def profile_name
return nil unless @scope_data.key?("profile")
return [@scope_data["profile"]["name_combined"], nil] if @scope_data["profile"].key?("name_combined")
[
@scope_data["profile"]["name_first"],
@scope_data["profile"]["name_last"]
]
end
end
end
require "magentasso/version"