Created
September 20, 2018 17:13
-
-
Save JohnLBevan/8035039d1bf06265746e90df0664bf52 to your computer and use it in GitHub Desktop.
OAuth2 for ADFS; without using the .Net ADAL library
This file contains hidden or 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
| <# | |
| Since we have a few systems which can't ues .Net or Java libraries; but rather rely on raw | |
| HTTP requests/response behaviour, I needed to find a way to demonstrate how to connect to | |
| O365 services without making use of Microsoft.IdentityModel.Clients.ActiveDirectory.dll | |
| An alternative approach is to use a client secret; however we didn't have access to this / | |
| had to work out a quick solution to progress with PoC work using the information available to us | |
| The below code effectively simulates this call: | |
| Clear-Host | |
| Add-Type -Path 'C:\temp\Microsoft.IdentityModel.Clients.ActiveDirectory.dll' #NB: I copy/pasted this DLL to somewhere easily accessible on my local machine | |
| #configuration for our app | |
| $clientId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' | |
| $tenantId = 'myCompany.com' | |
| $resourceId = 'https://myTestEnvironment.sandbox.operations.dynamics.com' | |
| $username = 'myUPN@myCompany.com' | |
| $password = 'myPa$$w0rd' | |
| $login = 'https://login.windows.net' | |
| #code to get the access token | |
| [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 | |
| $authContext = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext ("{0}/{1}" -f $login,$tenantId) | |
| $credential = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.UserCredential ($username, $password) | |
| $authenticationResult = $authContext.AcquireToken($resourceId, $clientID, $credential) | |
| $token = $authenticationResult.AccessToken | |
| #then we could call services like so | |
| $headers = @{ | |
| 'Content-Type' = 'application/json' | |
| Authorization = ('Bearer {0}' -f $token) | |
| } | |
| Invoke-RestMethod -Method 'Get' -Uri ('{0}/Metadata/DataEntities' -f $resourceId) -Headers $headers | ConvertTo-Json | |
| #> | |
| function Get-AdalAuthToken { | |
| [CmdletBinding()] | |
| Param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$TenantId | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [string]$Username | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [string]$Password #NB: to be compliant with PS rules we should use a PSCredential object; however since this code is to demonstrate the ADAL process without using anything that can't be easily recreated in other languages just using a plain string | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [Guid]$ClientId | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [string]$Resource | |
| , | |
| [Parameter(Mandatory = $false)] | |
| [string]$LoginUri = 'https://login.windows.net' | |
| ) | |
| Begin { | |
| [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 | |
| } | |
| Process { | |
| $clientRequestId = [Guid]::NewGuid() | |
| $adfsInfo = Get-AdfsAuthInfo -Username $Username -ClientRequestId $clientRequestId -LoginUri $LoginUri #there's probably a whole load of options we could take depending on what's returned here & in subsequent steps; I'm just implementing the path that I sniffed / that's required for this to work for my company/scenario | |
| $adfsMex = Get-AdfsMetadataExchangeInfo -AdfsAuthInfo $adfsInfo | |
| $adfsCert = Get-AdfsAuthCertificate -AdfsAuthInfo $adfsInfo -AdfsAuthMex $adfsMex -Username $Username -Password $Password | |
| Get-AdalOAuth2Token -TenantId $TenantId -ClientRequestId $clientRequestId -ClientId $ClientId -Resource $Resource -AdfsAuthCert $adfsCert -LoginUri $LoginUri | |
| } | |
| } | |
| #when we contact login.windows.net it redirects us to our company's ADFS site | |
| function Get-AdfsAuthInfo { | |
| [CmdletBinding()] | |
| Param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$Username | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [Guid]$ClientRequestId | |
| , | |
| [Parameter(Mandatory = $false)] | |
| [string]$LoginUri = 'https://login.windows.net' | |
| ) | |
| Process { | |
| $uri = '{0}/common/UserRealm/{1}?api-version=1.0' -f $LoginUri, $Username | |
| Invoke-RestMethod -Method 'Get' -Uri $uri -Headers @{'client-request-id'=$ClientRequestId;'return-client-request-id' = $True} -ContentType 'application/json' | |
| } | |
| } | |
| function Get-AdfsMetadataExchangeInfo { | |
| [CmdletBinding()] | |
| Param ( | |
| [Parameter(Mandatory = $true)] | |
| [PSObject]$AdfsAuthInfo | |
| ) | |
| Process { | |
| Invoke-RestMethod -Method Get -Uri ($AdfsAuthInfo.federation_metadata_url) -ContentType 'application/soap+xml' -UseBasicParsing | |
| } | |
| } | |
| function Get-AdfsAuthCertificate { | |
| [CmdletBinding()] | |
| Param ( | |
| [Parameter(Mandatory = $true)] | |
| [PSObject]$AdfsAuthInfo | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [PSObject]$AdfsAuthMex | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [string]$Username | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [string]$Password | |
| ) | |
| Begin { | |
| $bindingName = 'UserNameWSTrustBinding_IWSTrust13Async' | |
| #NB: lots of values in the below XML can be found in the AdfsAuthMex's policy; for now leaving many hardcoded as below / will adapt when testing if a need arises... | |
| $xmlBody = @' | |
| <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'>{0}</a:Action> | |
| <a:messageID>urn:uuid:{1}</a:messageID> | |
| <a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo> | |
| <a:To s:mustUnderstand='1'>{2}</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'> | |
| <u:Timestamp u:Id='_0'> | |
| <u:Created>{3:yyyy-MM-ddTHH:mm:ss.fffZ}</u:Created> | |
| <u:Expires>{4:yyyy-MM-ddTHH:mm:ss.fffZ}</u:Expires> | |
| </u:Timestamp> | |
| <o:UsernameToken u:Id='uuid-{5}'> | |
| <o:Username>{6}</o:Username> | |
| <o:Password>{7}</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'> | |
| <a:EndpointReference> | |
| <a:Address>{8}</a:Address> | |
| </a: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> | |
| '@ | |
| } | |
| Process { | |
| $EndpointAddress = $AdfsAuthMex.definitions.service.port | Where-Object {$_.name -eq $bindingName} | ForEach-Object {$_.EndpointReference.Address} | |
| $Operation = $AdfsAuthMex.definitions.binding | Where-Object {$_.name -eq $bindingName} | ForEach-Object {$_.operation.operation.soapAction} | |
| $messageId = [Guid]::NewGuid() | |
| $timeNow = (Get-Date).ToUniversalTime() | |
| $timeExpire = $timeNow.AddMinutes(10) | |
| $uuid = [Guid]::NewGuid() | |
| $endpointReferenceAddress = $AdfsAuthInfo.cloud_audience_urn | |
| $body = ($xmlBody -f $Operation, $messageId, $EndpointAddress, $timeNow, $timeExpire, $uuid, $Username, $Password, $endpointReferenceAddress) | |
| #use web request as we want to keep the response as XML rather than parse it to PS | |
| (Invoke-WebRequest -Method Post -Uri $EndpointAddress -Headers @{SOAPAction=$Operation} -ContentType 'application/soap+xml' -Body $body).Content | |
| } | |
| } | |
| function Get-AdalOAuth2Token { | |
| [CmdletBinding()] | |
| Param ( | |
| [Parameter(Mandatory = $true)] | |
| [string]$TenantId | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [Guid]$ClientRequestId | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [Guid]$ClientId | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [string]$Resource | |
| , | |
| [Parameter(Mandatory = $true)] | |
| [Xml]$AdfsAuthCert | |
| , | |
| [Parameter(Mandatory = $false)] | |
| [string]$LoginUri = 'https://login.windows.net' | |
| ) | |
| Process { | |
| $uri = [Uri]::new([Uri]::new([Uri]::new($LoginUri + '/'),$TenantId + '/'),'oauth2/token/').ToString() | |
| $headers = @{'client-request-id'=$ClientRequestId;'return-client-request-id' = $True; 'x-client-last-endpoint'='user_realm'; 'x-client-last-request' = $ClientRequestId} | |
| $grantType = 'urn:ietf:params:oauth:grant-type:saml1_1-bearer' | |
| $assertion = Get-OAuth2Assertion -AdfsAuthCert $AdfsAuthCert | |
| $scope = 'openid' | |
| $body = ('resource={0}&client_id={1}&grant_type={2}&assertion={3}&scope={4}' -f ([Uri]::EscapeDataString($Resource)), [Uri]::EscapeDataString($ClientId), [Uri]::EscapeDataString($grantType), ($assertion), [Uri]::EscapeDataString($scope)) | |
| Invoke-RestMethod -Method POST -Uri $uri -Headers $headers -ContentType 'application/x-www-form-urlencoded' -Body $body | |
| } | |
| } | |
| function Get-OAuth2Assertion { | |
| [CmdletBinding()] | |
| Param ( | |
| [Parameter(Mandatory = $true)] | |
| [Xml]$AdfsAuthCert | |
| ) | |
| Begin { | |
| } | |
| Process { | |
| [xml]$assertXml = $AdfsAuthCert.SelectSingleNode('/*[local-name()=''Envelope'']/*[local-name()=''Body'']/*[local-name()=''RequestSecurityTokenResponseCollection'']/*[local-name()=''RequestSecurityTokenResponse'']/*[local-name()=''RequestedSecurityToken'']/*[local-name()=''Assertion'']').OuterXml | |
| [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($assertXml.ToString())) | |
| } | |
| } | |
| $authenticationResult = Get-AdalAuthToken -TenantId 'myCompany.com' -Username 'myUPN@myCompany.com' -Password 'myPa$$w0rd' -ClientId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' -Resource 'https://myTestEnvironment.sandbox.operations.dynamics.com' | |
| $token = $authenticationResult.AccessToken | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment