#######################################################################
# FILE: windowslivelogin.rb
#
# DESCRIPTION: Sample implementation of Web Authentication and
# Delegated Authentication protocol in Ruby. Also
# includes trusted sign-in and application verification
# sample implementations.
#
# VERSION: 1.1
#
# Copyright (c) 2008 Microsoft Corporation. All Rights Reserved.
#######################################################################
require 'cgi'
require 'uri'
require 'base64'
require 'openssl'
require 'net/https'
require 'rexml/document'
module OmniAuth; module Strategies; class WindowsLive; class WindowsLiveLogin
#####################################################################
# Stub implementation for logging errors. If you want to enable
# debugging output using the default mechanism, specify true.
# By default, debug information will be printed to the standard
# error output and should be visible in the web server logs.
#####################################################################
def setDebug(flag)
@debug = flag
end
#####################################################################
# Stub implementation for logging errors. By default, this function
# does nothing if the debug flag has not been set with setDebug.
# Otherwise, it tries to log the error message.
#####################################################################
def debug(error)
return unless @debug
return if error.nil? or error.empty?
warn("Windows Live ID Authentication SDK #{error}")
nil
end
#####################################################################
# Stub implementation for handling a fatal error.
#####################################################################
def fatal(error)
debug(error)
raise(error)
end
#####################################################################
# Initialize the WindowsLiveLogin module with the application ID,
# secret key, and security algorithm.
#
# We recommend that you employ strong measures to protect the
# secret key. The secret key should never be exposed to the Web
# or other users.
#
# Be aware that if you do not supply these settings at
# initialization time, you may need to set the corresponding
# properties manually.
#
# For Delegated Authentication, you may optionally specify the
# privacy policy URL and return URL. If you do not specify these
# values here, the default values that you specified when you
# registered your application will be used.
#
# The 'force_delauth_nonprovisioned' flag also indicates whether
# your application is registered for Delegated Authentication
# (that is, whether it uses an application ID and secret key). We
# recommend that your Delegated Authentication application always
# be registered for enhanced security and functionality.
#####################################################################
def initialize(appid=nil, secret=nil, securityalgorithm=nil,
force_delauth_nonprovisioned=nil,
policyurl=nil, returnurl=nil)
self.force_delauth_nonprovisioned = force_delauth_nonprovisioned
self.appid = appid if appid
self.secret = secret if secret
self.securityalgorithm = securityalgorithm if securityalgorithm
self.policyurl = policyurl if policyurl
self.returnurl = returnurl if returnurl
end
#####################################################################
# Initialize the WindowsLiveLogin module from a settings file.
#
# 'settingsFile' specifies the location of the XML settings file
# that contains the application ID, secret key, and security
# algorithm. The file is of the following format:
#
#
# APPID
# SECRET
# wsignin1.0
#
#
# In a Delegated Authentication scenario, you may also specify
# 'returnurl' and 'policyurl' in the settings file, as shown in the
# Delegated Authentication samples.
#
# We recommend that you store the WindowsLiveLogin settings file
# in an area on your server that cannot be accessed through the
# Internet. This file contains important confidential information.
#####################################################################
def self.initFromXml(settingsFile)
o = self.new
settings = o.parseSettings(settingsFile)
o.setDebug(settings['debug'] == 'true')
o.force_delauth_nonprovisioned =
(settings['force_delauth_nonprovisioned'] == 'true')
o.appid = settings['appid']
o.secret = settings['secret']
o.oldsecret = settings['oldsecret']
o.oldsecretexpiry = settings['oldsecretexpiry']
o.securityalgorithm = settings['securityalgorithm']
o.policyurl = settings['policyurl']
o.returnurl = settings['returnurl']
o.baseurl = settings['baseurl']
o.secureurl = settings['secureurl']
o.consenturl = settings['consenturl']
o
end
#####################################################################
# Sets the application ID. Use this method if you did not specify
# an application ID at initialization.
#####################################################################
def appid=(appid)
if (appid.nil? or appid.empty?)
return if force_delauth_nonprovisioned
fatal("Error: appid: Null application ID.")
end
if (not appid =~ /^\w+$/)
fatal("Error: appid: Application ID must be alpha-numeric: " + appid)
end
@appid = appid
end
#####################################################################
# Returns the application ID.
#####################################################################
def appid
if (@appid.nil? or @appid.empty?)
fatal("Error: appid: App ID was not set. Aborting.")
end
@appid
end
#####################################################################
# Sets your secret key. Use this method if you did not specify
# a secret key at initialization.
#####################################################################
def secret=(secret)
if (secret.nil? or secret.empty?)
return if force_delauth_nonprovisioned
fatal("Error: secret=: Secret must be non-null.")
end
if (secret.size < 16)
fatal("Error: secret=: Secret must be at least 16 characters.")
end
@signkey = derive(secret, "SIGNATURE")
@cryptkey = derive(secret, "ENCRYPTION")
end
#####################################################################
# Sets your old secret key.
#
# Use this property to set your old secret key if you are in the
# process of transitioning to a new secret key. You may need this
# property because the Windows Live ID servers can take up to
# 24 hours to propagate a new secret key after you have updated
# your application settings.
#
# If an old secret key is specified here and has not expired
# (as determined by the oldsecretexpiry setting), it will be used
# as a fallback if token decryption fails with the new secret
# key.
#####################################################################
def oldsecret=(secret)
return if (secret.nil? or secret.empty?)
if (secret.size < 16)
fatal("Error: oldsecret=: Secret must be at least 16 characters.")
end
@oldsignkey = derive(secret, "SIGNATURE")
@oldcryptkey = derive(secret, "ENCRYPTION")
end
#####################################################################
# Sets the expiry time for your old secret key.
#
# After this time has passed, the old secret key will no longer be
# used even if token decryption fails with the new secret key.
#
# The old secret expiry time is represented as the number of seconds
# elapsed since January 1, 1970.
#####################################################################
def oldsecretexpiry=(timestamp)
return if (timestamp.nil? or timestamp.empty?)
timestamp = timestamp.to_i
fatal("Error: oldsecretexpiry=: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
@oldsecretexpiry = Time.at timestamp
end
#####################################################################
# Gets the old secret key expiry time.
#####################################################################
attr_accessor :oldsecretexpiry
#####################################################################
# Sets or gets the version of the security algorithm being used.
#####################################################################
attr_accessor :securityalgorithm
def securityalgorithm
if(@securityalgorithm.nil? or @securityalgorithm.empty?)
"wsignin1.0"
else
@securityalgorithm
end
end
#####################################################################
# Sets a flag that indicates whether Delegated Authentication
# is non-provisioned (i.e. does not use an application ID or secret
# key).
#####################################################################
attr_accessor :force_delauth_nonprovisioned
#####################################################################
# Sets the privacy policy URL, to which the Windows Live ID consent
# service redirects users to view the privacy policy of your Web
# site for Delegated Authentication.
#####################################################################
def policyurl=(policyurl)
if ((policyurl.nil? or policyurl.empty?) and force_delauth_nonprovisioned)
fatal("Error: policyurl=: Invalid policy URL specified.")
end
@policyurl = policyurl
end
#####################################################################
# Gets the privacy policy URL for your site.
#####################################################################
def policyurl
if (@policyurl.nil? or @policyurl.empty?)
debug("Warning: In the initial release of Del Auth, a Policy URL must be configured in the SDK for both provisioned and non-provisioned scenarios.")
raise("Error: policyurl: Policy URL must be set in a Del Auth non-provisioned scenario. Aborting.") if force_delauth_nonprovisioned
end
@policyurl
end
#####################################################################
# Sets the return URL--the URL on your site to which the consent
# service redirects users (along with the action, consent token,
# and application context) after they have successfully provided
# consent information for Delegated Authentication. This value will
# override the return URL specified during registration.
#####################################################################
def returnurl=(returnurl)
if ((returnurl.nil? or returnurl.empty?) and force_delauth_nonprovisioned)
fatal("Error: returnurl=: Invalid return URL specified.")
end
@returnurl = returnurl
end
#####################################################################
# Returns the return URL of your site.
#####################################################################
def returnurl
if ((@returnurl.nil? or @returnurl.empty?) and force_delauth_nonprovisioned)
fatal("Error: returnurl: Return URL must be set in a Del Auth non-provisioned scenario. Aborting.")
end
@returnurl
end
#####################################################################
# Sets or gets the base URL to use for the Windows Live Login server. You
# should not have to change this property. Furthermore, we recommend
# that you use the Sign In control instead of the URL methods
# provided here.
#####################################################################
attr_accessor :baseurl
def baseurl
if(@baseurl.nil? or @baseurl.empty?)
"http://login.live.com/"
else
@baseurl
end
end
#####################################################################
# Sets or gets the secure (HTTPS) URL to use for the Windows Live Login
# server. You should not have to change this property.
#####################################################################
attr_accessor :secureurl
def secureurl
if(@secureurl.nil? or @secureurl.empty?)
"https://login.live.com/"
else
@secureurl
end
end
#####################################################################
# Sets or gets the Consent Base URL to use for the Windows Live Consent
# server. You should not have to use or change this property directly.
#####################################################################
attr_accessor :consenturl
def consenturl
if(@consenturl.nil? or @consenturl.empty?)
"https://consent.live.com/"
else
@consenturl
end
end
end
#######################################################################
# Implementation of the basic methods needed for Web Authentication.
#######################################################################
class WindowsLiveLogin
#####################################################################
# Returns the sign-in URL to use for the Windows Live Login server.
# We recommend that you use the Sign In control instead.
#
# If you specify it, 'context' will be returned as-is in the sign-in
# response for site-specific use.
#####################################################################
def getLoginUrl(context=nil, market=nil)
url = baseurl + "wlogin.srf?appid=#{appid}"
url += "&alg=#{securityalgorithm}"
url += "&appctx=#{CGI.escape(context)}" if context
url += "&mkt=#{CGI.escape(market)}" if market
url
end
#####################################################################
# Returns the sign-out URL to use for the Windows Live Login server.
# We recommend that you use the Sign In control instead.
#####################################################################
def getLogoutUrl(market=nil)
url = baseurl + "logout.srf?appid=#{appid}"
url += "&mkt=#{CGI.escape(market)}" if market
url
end
#####################################################################
# Holds the user information after a successful sign-in.
#
# 'timestamp' is the time as obtained from the SSO token.
# 'id' is the pairwise unique ID for the user.
# 'context' is the application context that was originally passed to
# the sign-in request, if any.
# 'token' is the encrypted Web Authentication token that contains the
# UID. This can be cached in a cookie and the UID can be retrieved by
# calling the processToken method.
# 'usePersistentCookie?' indicates whether the application is
# expected to store the user token in a session or persistent
# cookie.
#####################################################################
class User
attr_reader :timestamp, :id, :context, :token
def usePersistentCookie?
@usePersistentCookie
end
#####################################################################
# Initialize the User with time stamp, userid, flags, context and token.
#####################################################################
def initialize(timestamp, id, flags, context, token)
self.timestamp = timestamp
self.id = id
self.flags = flags
self.context = context
self.token = token
end
private
attr_writer :timestamp, :id, :flags, :context, :token
#####################################################################
# Sets or gets the Unix timestamp as obtained from the SSO token.
#####################################################################
def timestamp=(timestamp)
raise("Error: User: Null timestamp in token.") unless timestamp
timestamp = timestamp.to_i
raise("Error: User: Invalid timestamp: #{timestamp}") if (timestamp <= 0)
@timestamp = Time.at timestamp
end
#####################################################################
# Sets or gets the pairwise unique ID for the user.
#####################################################################
def id=(id)
raise("Error: User: Null id in token.") unless id
raise("Error: User: Invalid id: #{id}") unless (id =~ /^\w+$/)
@id = id
end
#####################################################################
# Sets or gets the usePersistentCookie flag for the user.
#####################################################################
def flags=(flags)
@usePersistentCookie = false
if flags
@usePersistentCookie = ((flags.to_i % 2) == 1)
end
end
end
#####################################################################
# Processes the sign-in response from the Windows Live sign-in server.
#
# 'query' contains the preprocessed POST table, such as that
# returned by CGI.params or Rails. (The unprocessed POST string
# could also be used here but we do not recommend it).
#
# This method returns a User object on successful sign-in; otherwise
# it returns nil.
#####################################################################
def processLogin(query)
query = parse query
unless query
debug("Error: processLogin: Failed to parse query.")
return
end
action = query['action']
unless action == 'login'
debug("Warning: processLogin: query action ignored: #{action}.")
return
end
token = query['stoken']
context = CGI.unescape(query['appctx']) if query['appctx']
processToken(token, context)
end
#####################################################################
# Decodes and validates a Web Authentication token. Returns a User
# object on success. If a context is passed in, it will be returned
# as the context field in the User object.
#####################################################################
def processToken(token, context=nil)
if token.nil? or token.empty?
debug("Error: processToken: Null/empty token.")
return
end
stoken = decodeAndValidateToken token
stoken = parse stoken
unless stoken
debug("Error: processToken: Failed to decode/validate token: #{token}")
return
end
sappid = stoken['appid']
unless sappid == appid
debug("Error: processToken: Application ID in token did not match ours: #{sappid}, #{appid}")
return
end
begin
user = User.new(stoken['ts'], stoken['uid'], stoken['flags'],
context, token)
return user
rescue Exception => e
debug("Error: processToken: Contents of token considered invalid: #{e}")
return
end
end
#####################################################################
# Returns an appropriate content type and body response that the
# application handler can return to signify a successful sign-out
# from the application.
#
# When a user signs out of Windows Live or a Windows Live
# application, a best-effort attempt is made at signing the user out
# from all other Windows Live applications the user might be signed
# in to. This is done by calling the handler page for each
# application with 'action' set to 'clearcookie' in the query
# string. The application handler is then responsible for clearing
# any cookies or data associated with the sign-in. After successfully
# signing the user out, the handler should return a GIF (any GIF)
# image as response to the 'action=clearcookie' query.
#####################################################################
def getClearCookieResponse()
type = "image/gif"
content = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7"
content = Base64.decode64(content)
return type, content
end
end
#######################################################################
# Implementation of the basic methods needed for Delegated
# Authentication.
#######################################################################
class WindowsLiveLogin
#####################################################################
# Returns the consent URL to use for Delegated Authentication for
# the given comma-delimited list of offers.
#
# If you specify it, 'context' will be returned as-is in the consent
# response for site-specific use.
#
# The registered/configured return URL can also be overridden by
# specifying 'ru' here.
#
# You can change the language in which the consent page is displayed
# by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
# 'market' parameter.
#####################################################################
def getConsentUrl(offers, context=nil, ru=nil, market=nil)
if (offers.nil? or offers.empty?)
fatal("Error: getConsentUrl: Invalid offers list.")
end
url = consenturl + "Delegation.aspx?ps=#{CGI.escape(offers)}"
url += "&appctx=#{CGI.escape(context)}" if context
ru = returnurl if (ru.nil? or ru.empty?)
url += "&ru=#{CGI.escape(ru)}" if ru
pu = policyurl
url += "&pl=#{CGI.escape(pu)}" if pu
url += "&mkt=#{CGI.escape(market)}" if market
url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
url
end
#####################################################################
# Returns the URL to use to download a new consent token, given the
# offers and refresh token.
# The registered/configured return URL can also be overridden by
# specifying 'ru' here.
#####################################################################
def getRefreshConsentTokenUrl(offers, refreshtoken, ru)
if (offers.nil? or offers.empty?)
fatal("Error: getRefreshConsentTokenUrl: Invalid offers list.")
end
if (refreshtoken.nil? or refreshtoken.empty?)
fatal("Error: getRefreshConsentTokenUrl: Invalid refresh token.")
end
url = consenturl + "RefreshToken.aspx?ps=#{CGI.escape(offers)}"
url += "&reft=#{refreshtoken}"
ru = returnurl if (ru.nil? or ru.empty?)
url += "&ru=#{CGI.escape(ru)}" if ru
url += "&app=#{getAppVerifier()}" unless force_delauth_nonprovisioned
url
end
#####################################################################
# Returns the URL for the consent-management user interface.
# You can change the language in which the consent page is displayed
# by specifying a culture ID (For example, 'fr-fr' or 'en-us') in the
# 'market' parameter.
#####################################################################
def getManageConsentUrl(market=nil)
url = consenturl + "ManageConsent.aspx"
url += "?mkt=#{CGI.escape(market)}" if market
url
end
class ConsentToken
attr_reader :delegationtoken, :refreshtoken, :sessionkey, :expiry
attr_reader :offers, :offers_string, :locationid, :context
attr_reader :decodedtoken, :token
#####################################################################
# Indicates whether the delegation token is set and has not expired.
#####################################################################
def isValid?
return false unless delegationtoken
return ((Time.now.to_i-300) < expiry.to_i)
end
#####################################################################
# Refreshes the current token and replace it. If operation succeeds
# true is returned to signify success.
#####################################################################
def refresh
ct = @wll.refreshConsentToken(self)
return false unless ct
copy(ct)
true
end
#####################################################################
# Initialize the ConsentToken module with the WindowsLiveLogin,
# delegation token, refresh token, session key, expiry, offers,
# location ID, context, decoded token, and raw token.
#####################################################################
def initialize(wll, delegationtoken, refreshtoken, sessionkey, expiry,
offers, locationid, context, decodedtoken, token)
@wll = wll
self.delegationtoken = delegationtoken
self.refreshtoken = refreshtoken
self.sessionkey = sessionkey
self.expiry = expiry
self.offers = offers
self.locationid = locationid
self.context = context
self.decodedtoken = decodedtoken
self.token = token
end
private
attr_writer :delegationtoken, :refreshtoken, :sessionkey, :expiry
attr_writer :offers, :offers_string, :locationid, :context
attr_writer :decodedtoken, :token, :locationid
#####################################################################
# Sets the delegation token.
#####################################################################
def delegationtoken=(delegationtoken)
if (delegationtoken.nil? or delegationtoken.empty?)
raise("Error: ConsentToken: Null delegation token.")
end
@delegationtoken = delegationtoken
end
#####################################################################
# Sets the session key.
#####################################################################
def sessionkey=(sessionkey)
if (sessionkey.nil? or sessionkey.empty?)
raise("Error: ConsentToken: Null session key.")
end
@sessionkey = @wll.u64(sessionkey)
end
#####################################################################
# Sets the expiry time of the delegation token.
#####################################################################
def expiry=(expiry)
if (expiry.nil? or expiry.empty?)
raise("Error: ConsentToken: Null expiry time.")
end
expiry = expiry.to_i
raise("Error: ConsentToken: Invalid expiry: #{expiry}") if (expiry <= 0)
@expiry = Time.at expiry
end
#####################################################################
# Sets the offers/actions for which the user granted consent.
#####################################################################
def offers=(offers)
if (offers.nil? or offers.empty?)
raise("Error: ConsentToken: Null offers.")
end
@offers_string = ""
@offers = []
offers = CGI.unescape(offers)
offers = offers.split(";")
offers.each{|offer|
offer = offer.split(":")[0]
@offers_string += "," unless @offers_string.empty?
@offers_string += offer
@offers.push(offer)
}
end
#####################################################################
# Sets the LocationID.
#####################################################################
def locationid=(locationid)
if (locationid.nil? or locationid.empty?)
raise("Error: ConsentToken: Null Location ID.")
end
@locationid = locationid
end
#####################################################################
# Makes a copy of the ConsentToken object.
#####################################################################
def copy(consenttoken)
@delegationtoken = consenttoken.delegationtoken
@refreshtoken = consenttoken.refreshtoken
@sessionkey = consenttoken.sessionkey
@expiry = consenttoken.expiry
@offers = consenttoken.offers
@locationid = consenttoken.locationid
@offers_string = consenttoken.offers_string
@decodedtoken = consenttoken.decodedtoken
@token = consenttoken.token
end
end
#####################################################################
# Processes the POST response from the Delegated Authentication
# service after a user has granted consent. The processConsent
# function extracts the consent token string and returns the result
# of invoking the processConsentToken method.
#####################################################################
def processConsent(query)
query = parse query
unless query
debug("Error: processConsent: Failed to parse query.")
return
end
action = query['action']
unless action == 'delauth'
debug("Warning: processConsent: query action ignored: #{action}.")
return
end
responsecode = query['ResponseCode']
unless responsecode == 'RequestApproved'
debug("Error: processConsent: Consent was not successfully granted: #{responsecode}")
return
end
token = query['ConsentToken']
context = CGI.unescape(query['appctx']) if query['appctx']
processConsentToken(token, context)
end
#####################################################################
# Processes the consent token string that is returned in the POST
# response by the Delegated Authentication service after a
# user has granted consent.
#####################################################################
def processConsentToken(token, context=nil)
if token.nil? or token.empty?
debug("Error: processConsentToken: Null token.")
return
end
decodedtoken = token
parsedtoken = parse(CGI.unescape(decodedtoken))
unless parsedtoken
debug("Error: processConsentToken: Failed to parse token: #{token}")
return
end
eact = parsedtoken['eact']
if eact
decodedtoken = decodeAndValidateToken eact
unless decodedtoken
debug("Error: processConsentToken: Failed to decode/validate token: #{token}")
return
end
parsedtoken = parse(decodedtoken)
decodedtoken = CGI.escape(decodedtoken)
end
begin
consenttoken = ConsentToken.new(self,
parsedtoken['delt'],
parsedtoken['reft'],
parsedtoken['skey'],
parsedtoken['exp'],
parsedtoken['offer'],
parsedtoken['lid'],
context, decodedtoken, token)
return consenttoken
rescue Exception => e
debug("Error: processConsentToken: Contents of token considered invalid: #{e}")
return
end
end
#####################################################################
# Attempts to obtain a new, refreshed token and return it. The
# original token is not modified.
#####################################################################
def refreshConsentToken(consenttoken, ru=nil)
if consenttoken.nil?
debug("Error: refreshConsentToken: Null consent token.")
return
end
refreshConsentToken2(consenttoken.offers_string, consenttoken.refreshtoken, ru)
end
#####################################################################
# Helper function to obtain a new, refreshed token and return it.
# The original token is not modified.
#####################################################################
def refreshConsentToken2(offers_string, refreshtoken, ru=nil)
url = nil
begin
url = getRefreshConsentTokenUrl(offers_string, refreshtoken, ru)
ret = fetch url
ret.value # raises exception if fetch failed
body = ret.body
body.scan(/\{"ConsentToken":"(.*)"\}/){|match|
return processConsentToken("#{match}")
}
debug("Error: refreshConsentToken2: Failed to extract token: #{body}")
rescue Exception => e
debug("Error: Failed to refresh consent token: #{e}")
end
return
end
end
#######################################################################
# Common methods.
#######################################################################
class WindowsLiveLogin
#####################################################################
# Decodes and validates the token.
#####################################################################
def decodeAndValidateToken(token, cryptkey=@cryptkey, signkey=@signkey,
internal_allow_recursion=true)
haveoldsecret = false
if (oldsecretexpiry and (Time.now.to_i < oldsecretexpiry.to_i))
haveoldsecret = true if (@oldcryptkey and @oldsignkey)
end
haveoldsecret = (haveoldsecret and internal_allow_recursion)
stoken = decodeToken(token, cryptkey)
stoken = validateToken(stoken, signkey) if stoken
if (stoken.nil? and haveoldsecret)
debug("Warning: Failed to validate token with current secret, attempting old secret.")
stoken = decodeAndValidateToken(token, @oldcryptkey, @oldsignkey, false)
end
stoken
end
#####################################################################
# Decodes the given token string; returns undef on failure.
#
# First, the string is URL-unescaped and base64 decoded.
# Second, the IV is extracted from the first 16 bytes of the string.
# Finally, the string is decrypted using the encryption key.
#####################################################################
def decodeToken(token, cryptkey=@cryptkey)
if (cryptkey.nil? or cryptkey.empty?)
fatal("Error: decodeToken: Secret key was not set. Aborting.")
end
token = u64(token)
if (token.nil? or (token.size <= 16) or !(token.size % 16).zero?)
debug("Error: decodeToken: Attempted to decode invalid token.")
return
end
iv = token[0..15]
crypted = token[16..-1]
begin
aes128cbc = OpenSSL::Cipher::AES128.new("CBC")
aes128cbc.decrypt
aes128cbc.iv = iv
aes128cbc.key = cryptkey
decrypted = aes128cbc.update(crypted) + aes128cbc.final
rescue Exception => e
debug("Error: decodeToken: Decryption failed: #{token}, #{e}")
return
end
decrypted
end
#####################################################################
# Creates a signature for the given string by using the signature
# key.
#####################################################################
def signToken(token, signkey=@signkey)
if (signkey.nil? or signkey.empty?)
fatal("Error: signToken: Secret key was not set. Aborting.")
end
begin
digest = OpenSSL::Digest::SHA256.new
return OpenSSL::HMAC.digest(digest, signkey, token)
rescue Exception => e
debug("Error: signToken: Signing failed: #{token}, #{e}")
return
end
end
#####################################################################
# Extracts the signature from the token and validates it.
#####################################################################
def validateToken(token, signkey=@signkey)
if (token.nil? or token.empty?)
debug("Error: validateToken: Null token.")
return
end
body, sig = token.split("&sig=")
if (body.nil? or sig.nil?)
debug("Error: validateToken: Invalid token: #{token}")
return
end
sig = u64(sig)
return token if (sig == signToken(body, signkey))
debug("Error: validateToken: Signature did not match.")
return
end
end
#######################################################################
# Implementation of the methods needed to perform Windows Live
# application verification as well as trusted sign-in.
#######################################################################
class WindowsLiveLogin
#####################################################################
# Generates an application verifier token. An IP address can
# optionally be included in the token.
#####################################################################
def getAppVerifier(ip=nil)
token = "appid=#{appid}&ts=#{timestamp}"
token += "&ip=#{ip}" if ip
token += "&sig=#{e64(signToken(token))}"
CGI.escape token
end
#####################################################################
# Returns the URL that is required to retrieve the application
# security token.
#
# By default, the application security token is generated for
# the Windows Live site; a specific Site ID can optionally be
# specified in 'siteid'. The IP address can also optionally be
# included in 'ip'.
#
# If 'js' is nil, a JavaScript Output Notation (JSON) response is
# returned in the following format:
#
# {"token":""}
#
# Otherwise, a JavaScript response is returned. It is assumed that
# WLIDResultCallback is a custom function implemented to handle the
# token value:
#
# WLIDResultCallback("");
#####################################################################
def getAppLoginUrl(siteid=nil, ip=nil, js=nil)
url = secureurl + "wapplogin.srf?app=#{getAppVerifier(ip)}"
url += "&alg=#{securityalgorithm}"
url += "&id=#{siteid}" if siteid
url += "&js=1" if js
url
end
#####################################################################
# Retrieves the application security token for application
# verification from the application sign-in URL.
#
# By default, the application security token will be generated for
# the Windows Live site; a specific Site ID can optionally be
# specified in 'siteid'. The IP address can also optionally be
# included in 'ip'.
#
# Implementation note: The application security token is downloaded
# from the application sign-in URL in JSON format:
#
# {"token":""}
#
# Therefore we must extract from the string and return it as
# seen here.
#####################################################################
def getAppSecurityToken(siteid=nil, ip=nil)
url = getAppLoginUrl(siteid, ip)
begin
ret = fetch url
ret.value # raises exception if fetch failed
body = ret.body
body.scan(/\{"token":"(.*)"\}/){|match|
return match
}
debug("Error: getAppSecurityToken: Failed to extract token: #{body}")
rescue Exception => e
debug("Error: getAppSecurityToken: Failed to get token: #{e}")
end
return
end
#####################################################################
# Returns a string that can be passed to the getTrustedParams
# function as the 'retcode' parameter. If this is specified as the
# 'retcode', the application will be used as return URL after it
# finishes trusted sign-in.
#####################################################################
def getAppRetCode
"appid=#{appid}"
end
#####################################################################
# Returns a table of key-value pairs that must be posted to the
# sign-in URL for trusted sign-in. Use HTTP POST to do this. Be aware
# that the values in the table are neither URL nor HTML escaped and
# may have to be escaped if you are inserting them in code such as
# an HTML form.
#
# The user to be trusted on the local site is passed in as string
# 'user'.
#
# Optionally, 'retcode' specifies the resource to which successful
# sign-in is redirected, such as Windows Live Mail, and is typically
# a string in the format 'id=2000'. If you pass in the value from
# getAppRetCode instead, sign-in will be redirected to the
# application. Otherwise, an HTTP 200 response is returned.
#####################################################################
def getTrustedParams(user, retcode=nil)
token = getTrustedToken(user)
return unless token
token = %{#{token}uri:WindowsLiveID}
params = {}
params['wa'] = securityalgorithm
params['wresult'] = token
params['wctx'] = retcode if retcode
params
end
#####################################################################
# Returns the trusted sign-in token in the format that is needed by a
# control doing trusted sign-in.
#
# The user to be trusted on the local site is passed in as string
# 'user'.
#####################################################################
def getTrustedToken(user)
if user.nil? or user.empty?
debug('Error: getTrustedToken: Null user specified.')
return
end
token = "appid=#{appid}&uid=#{CGI.escape(user)}&ts=#{timestamp}"
token += "&sig=#{e64(signToken(token))}"
CGI.escape token
end
#####################################################################
# Returns the trusted sign-in URL to use for the Windows Live Login
# server.
#####################################################################
def getTrustedLoginUrl
secureurl + "wlogin.srf"
end
#####################################################################
# Returns the trusted sign-out URL to use for the Windows Live Login
# server.
#####################################################################
def getTrustedLogoutUrl
secureurl + "logout.srf?appid=#{appid}"
end
end
#######################################################################
# Helper methods.
#######################################################################
class WindowsLiveLogin
#######################################################################
# Function to parse the settings file.
#######################################################################
def parseSettings(settingsFile)
settings = {}
begin
file = File.new(settingsFile)
doc = REXML::Document.new file
root = doc.root
root.each_element{|e|
settings[e.name] = e.text
}
rescue Exception => e
fatal("Error: parseSettings: Error while reading #{settingsFile}: #{e}")
end
return settings
end
#####################################################################
# Derives the key, given the secret key and prefix as described in the
# Web Authentication SDK documentation.
#####################################################################
def derive(secret, prefix)
begin
fatal("Nil/empty secret.") if (secret.nil? or secret.empty?)
key = prefix + secret
key = OpenSSL::Digest::SHA256.digest(key)
return key[0..15]
rescue Exception => e
debug("Error: derive: #{e}")
return
end
end
#####################################################################
# Parses query string and return a table
# {String=>String}
#
# If a table is passed in from CGI.params, we convert it from
# {String=>[]} to {String=>String}. I believe Rails uses symbols
# instead of strings in general, so we convert from symbols to
# strings here also.
#####################################################################
def parse(input)
if (input.nil? or input.empty?)
debug("Error: parse: Nil/empty input.")
return
end
pairs = {}
if (input.class == String)
input = input.split('&')
input.each{|pair|
k, v = pair.split('=')
pairs[k] = v
}
else
input.each{|k, v|
v = v[0] if (v.class == Array)
pairs[k.to_s] = v.to_s
}
end
return pairs
end
#####################################################################
# Generates a time stamp suitable for the application verifier token.
#####################################################################
def timestamp
Time.now.to_i.to_s
end
#####################################################################
# Base64-encodes and URL-escapes a string.
#####################################################################
def e64(s)
return unless s
CGI.escape Base64.encode64(s)
end
#####################################################################
# URL-unescapes and Base64-decodes a string.
#####################################################################
def u64(s)
return unless s
Base64.decode64 CGI.unescape(s)
end
#####################################################################
# Fetches the contents given a URL.
#####################################################################
def fetch(url)
url = URI.parse url
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = (url.scheme == "https")
http.request_get url.request_uri
end
end end end end