Created
June 9, 2023 15:51
-
-
Save ukdave/b26d366e8ab5eef3d61c6cc51e01bf6c to your computer and use it in GitHub Desktop.
Ruby Sharepoint SAML federated authentication
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
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> | |
<s:Header> | |
<a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action> | |
<a:ReplyTo> | |
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> | |
</a:ReplyTo> | |
<a:To s:mustUnderstand="1"><%= to %></a:To> | |
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> | |
<o:UsernameToken u:Id="uuid-7b105801-44ac-4da7-aa69-a87f9db37299-1"> | |
<o:Username><%= username.encode(xml: :text) %></o:Username> | |
<o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"><%= password.encode(xml: :text) %></o:Password> | |
</o:UsernameToken> | |
</o:Security> | |
</s:Header> | |
<s:Body> | |
<trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512"> | |
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"> | |
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing"> | |
<wsa:Address><%= relying_party %></wsa:Address> | |
</wsa:EndpointReference> | |
</wsp:AppliesTo> | |
<trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer</trust:KeyType> | |
<trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType> | |
</trust:RequestSecurityToken> | |
</s:Body> | |
</s:Envelope> |
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
# frozen_string_literal: true | |
class AuthenticationError < StandardError | |
attr_reader :response | |
def initialize(message, response) | |
super(message) | |
@response = response | |
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
# frozen_string_literal: true | |
# Heavily based on: https://github.com/s-KaiNet/node-sp-auth | |
# | |
# The 5 steps are: | |
# | |
# 1. get user realm details from microsoft | |
# 2. get the assertion from your SAML IDP (identity provider) | |
# 3. trade the assertion for a STS (Security Token Service) token | |
# 4. trade the STS token for cookies | |
# 5. use the cookies to do your REST calls | |
# | |
# Usage: | |
# Authenticator.call("https://example.sharepoint.com", "user@example.com", "Pa55w0rd") | |
# => { "rtFa" => "xxxxx", "FedAuth" => "xxxxx" } | |
# | |
# See also: | |
# * https://stackoverflow.com/questions/11295953/claim-auth-from-adfs | |
# * https://github.com/Plaristote/sharepoint-ruby | |
class Authenticator | |
CONTENT_TYPES = { | |
soap: "application/soap+xml; charset=utf-8", | |
form: "application/x-www-form-urlencoded" | |
}.freeze | |
class << self | |
def call(site, username, password) | |
user_realm = fetch_user_realm(username) | |
unless user_realm["NameSpaceType"] == "Federated" | |
raise build_auth_error("Unsupported realm type: #{user_realm['NameSpaceType']}", nil) | |
end | |
saml_assertion = fetch_adfs_saml_assertion(username, password, user_realm["AuthURL"], | |
user_realm["CloudInstanceIssuerUri"]) | |
sts_token = fetch_sts_token(saml_assertion, site) | |
fetch_cookies(site, sts_token) | |
end | |
private | |
def fetch_user_realm(username) | |
response = HTTParty.post("https://login.microsoftonline.com/GetUserRealm.srf", body: { login: username }) | |
raise build_auth_error("Failed to fetch user realm", response) unless response.success? | |
response.parsed_response | |
end | |
def fetch_adfs_saml_assertion(username, password, adfs_url, relying_party) | |
adfs_host = URI.parse(adfs_url).host | |
username_mixed_url = "https://#{adfs_host}/adfs/services/trust/13/usernamemixed" | |
body = render_adfs_saml_request_template(username_mixed_url, username, password, relying_party) | |
response = perform_http_request(username_mixed_url, body, :soap) | |
raise build_auth_error("Authentication failed (ADFS)", response, "s:Text") unless response.success? | |
token = get_string_in_xml_tag(response.body, "trust:RequestedSecurityToken") | |
raise build_auth_error("Authentication failed (ADFS) - SAML assertion not found", response) if token.blank? | |
token | |
end | |
def fetch_sts_token(saml_assertion, site) | |
body = render_sts_token_request_template(saml_assertion, site) | |
response = perform_http_request("https://login.microsoftonline.com/extSTS.srf", body, :soap) | |
raise build_auth_error("Authentication failed (STS)", response, "psf:text") unless response.success? | |
token = get_string_in_xml_tag(response.body, "wsse:BinarySecurityToken") | |
raise build_auth_error("Authentication failed (STS) - security token not found", response) if token.blank? | |
token | |
end | |
def fetch_cookies(site, sts_token) | |
response = perform_http_request("#{site}/_forms/default.aspx?wa=wsignin1.0", sts_token, :form) | |
unless response.success? | |
raise build_auth_error("Authentication failed (cookies) - response code: #{response.code}", response) | |
end | |
header = response.headers["set-cookie"] | |
cookies = { "rtFa" => get_cookie_from_header(header, "rtFa"), | |
"FedAuth" => get_cookie_from_header(header, "FedAuth") } | |
return cookies if cookies["rtFa"].present? && cookies["FedAuth"].present? | |
raise build_auth_error("Authentication failed (cookies) - rtFa and/or FedAuth cookies not found", response) | |
end | |
def perform_http_request(url, body, content_type) | |
HTTParty.post(url, body:, headers: { "Content-Length" => body.length.to_s, | |
"Content-Type" => CONTENT_TYPES.fetch(content_type) }) | |
end | |
def render_adfs_saml_request_template(to, username, password, relying_party) | |
load_template("adfs_saml_request").result_with_hash(to:, username:, password:, relying_party:) | |
end | |
def render_sts_token_request_template(saml_assertion, endpoint) | |
load_template("sts_token_request").result_with_hash(saml_assertion:, endpoint:) | |
end | |
def load_template(name) | |
ERB.new(File.read(File.expand_path("#{name}.xml.erb", __dir__))) | |
end | |
def get_string_in_xml_tag(str, tag) | |
result = str.match(%r{<#{Regexp.escape(tag)}[^>]*>(.*)</#{Regexp.escape(tag)}>}) | |
return nil unless result | |
result.captures.first | |
end | |
def get_cookie_from_header(header, cookie_name) | |
result = header.match(/#{Regexp.escape(cookie_name)}=([^;]+);/) | |
return nil unless result | |
result.captures.first | |
end | |
def build_auth_error(message, response, xml_error_message_tag = nil) | |
full_message = message.dup | |
full_message.concat(" - response code: #{response.code}") if response.present? && !response.success? | |
full_message.concat(": #{get_string_in_xml_tag(response.body, xml_error_message_tag)}") if xml_error_message_tag | |
AuthenticationError.new(full_message, response) | |
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
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> | |
<s:Header> | |
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue</a:Action> | |
<a:ReplyTo> | |
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> | |
</a:ReplyTo> | |
<a:To s:mustUnderstand="1">https://login.microsoftonline.com/extSTS.srf</a:To> | |
<o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><%= saml_assertion %></o:Security> | |
</s:Header> | |
<s:Body> | |
<t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"> | |
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy"> | |
<a:EndpointReference> | |
<a:Address><%= endpoint %></a:Address> | |
</a:EndpointReference> | |
</wsp:AppliesTo> | |
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType> | |
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType> | |
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType> | |
</t:RequestSecurityToken> | |
</s:Body> | |
</s:Envelope> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment