From ba3594bd199029d61b6664c60b568d8ebe67589b Mon Sep 17 00:00:00 2001 From: Michael Bleigh Date: Mon, 17 Jan 2011 23:30:31 +0800 Subject: [PATCH] Adds (incomplete) Windows Live strategy to oa-more --- oa-more/lib/omniauth/more.rb | 2 +- .../lib/omniauth/strategies/windows_live.rb | 39 + .../windows_live/windowslivelogin.rb | 1143 +++++++++++++++++ 3 files changed, 1183 insertions(+), 1 deletion(-) create mode 100644 oa-more/lib/omniauth/strategies/windows_live.rb create mode 100644 oa-more/lib/omniauth/strategies/windows_live/windowslivelogin.rb diff --git a/oa-more/lib/omniauth/more.rb b/oa-more/lib/omniauth/more.rb index c1a41f7..ef94de3 100644 --- a/oa-more/lib/omniauth/more.rb +++ b/oa-more/lib/omniauth/more.rb @@ -2,6 +2,6 @@ require 'omniauth/core' module OmniAuth module Strategies - # autoload strategies here + autoload :WindowsLive, 'omniauth/strategies/windows_live' end end diff --git a/oa-more/lib/omniauth/strategies/windows_live.rb b/oa-more/lib/omniauth/strategies/windows_live.rb new file mode 100644 index 0000000..736b66f --- /dev/null +++ b/oa-more/lib/omniauth/strategies/windows_live.rb @@ -0,0 +1,39 @@ +require 'omniauth/core' +require 'omniauth/strategies/windows_live/windowslivelogin' + +module OmniAuth + module Strategies + class WindowsLive + include OmniAuth::Strategy + + attr_accessor :app_id, :app_secret + + # Initialize the strategy by providing + # + # @param app_id [String] The application ID from your registered app with Microsoft. + # @param app_secret [String] The secret from your registered app with Microsoft. + # @option options [String] :locale A localization string for the login, should be in the form `en-us` or similar. + # @option options [String] :state Some state information that is serialized into the query string upon callback. + # @option options [Boolean] :ssl Whether or not to use SSL for login. Defaults to `true`. + # @option options [Boolean] :force_nonprovisioned When true, forces a non-provisioned (i.e. no app id or secret) mode. + def initialize(app, app_id = nil, app_secret = nil, options = {}) + self.app_id = app_id + self.app_secret = app_secret + super(app, :windows_live, app_id, app_secret, options) + options[:ssl] ||= true + options[:locale] ||= 'en-us' + options[:force_nonprovisioned] = true unless app_id + end + + protected + + def consumer + WindowsLiveLogin.new app_id, app_secret, options[:security_algorithm], options[:force_nonprovisioned], options[:policy_url], callback_url + end + + def request_phase + redirect consumer.getLoginUrl(options[:state], options[:locale]) + end + end + end +end diff --git a/oa-more/lib/omniauth/strategies/windows_live/windowslivelogin.rb b/oa-more/lib/omniauth/strategies/windows_live/windowslivelogin.rb new file mode 100644 index 0000000..ba10291 --- /dev/null +++ b/oa-more/lib/omniauth/strategies/windows_live/windowslivelogin.rb @@ -0,0 +1,1143 @@ +####################################################################### +# 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 +