Skip to content

Instantly share code, notes, and snippets.

@JohnLBevan
Created September 20, 2018 17:13
Show Gist options
  • Select an option

  • Save JohnLBevan/8035039d1bf06265746e90df0664bf52 to your computer and use it in GitHub Desktop.

Select an option

Save JohnLBevan/8035039d1bf06265746e90df0664bf52 to your computer and use it in GitHub Desktop.
OAuth2 for ADFS; without using the .Net ADAL library
<#
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