«

»

Jul 21

Automated testing of a Provider Hosted App with PowerShell

Background

I was recently  engaged to assist a client in arranging for SharePoint search results to be surfaced in a Drupal PHP based content management system. Using the excellent blog post by Kirk Evans (High Trust SharePoint Apps on Non-Microsoft platforms) and some judicious use of Fiddler/NetMon we were able to get the Drupal Application authenticating and returning search results from SharePoint.

This post isn’t about that process though as my focus was purely on the SharePoint side of the fence, so I have no idea how the Drupal team managed the JWT creation or Active Directory integration. I do know it wasn’t without a significant amount of effort on their side! All I did was ensure that SharePoint was configured for apps and to create the .APP file that allowed their application request the permissions it needed.

Instead, this post deals with one of the ‘fine print’ requirements of the engagement, and thus assumes that you already have a provider hosted app registered in your environment, which is already configured to host Apps in an on-premises scenario.

As the Drupal website was provided and hosted by a third party, the IT team wanted to be able to test the applications connection into SharePoint in order to be able to prove a demarcation line in the event of problems in future.

Our requirements boiled down to:-

  • The solution needed to simulate the third party app as closely as possible
  • It needed to run on a daily schedule and provide e-mail alerts
  • It needed to be able to be run on-demand.

My go to tool usually for anything like this is PowerShell in conjunction with Task Scheduler and in this case that was my chosen scenario, especially as this particular client likes to shy away from custom development that requires console apps, custom dlls or anything with a costly lifecycle.

My initial intention was to run this in PowerShell 2.0 on a server that is provided for batch testing and the like, however I underestimated the poor certificate handling in PowerShell 2.0 which made signing the required JavaScript Web Tokens (JWT) nigh on impossible. After a good few failed attempts, I decided to break out Visual Studio and to try and unpick how the TokenHelper class that is deployed when you create a Provider Hosted App (Add-in) for SharePoint.

This pointed me towards the System.IdentityModel.Tokens.JWT.dll that is available as a NUGet package in VS. By pulling this down to a Windows Server 2012 box and making use of PowerShell 3.0, I was able to come up with a script that lets us automate the testing of the Provider Hosted App. In the end I ran this on one of our app servers to make use of the SharePoint e-mail handling.

In order to achieve this we will create a JWT that represents the app that has been registered in SharePoint and a suitable test account in Active Directory. This token will then be used as the Authorization Bearer token in a web request against the SharePoint Search REST Api endpoint.

In order to do this though, we need some information that is specific to each environment in order to build the JWT. Kirk covers this really well in his post, so I shall re-surface his explanation here, but credit is his for these tables.

Outer Token

aud Audience.  The value is 00000003-0000-0ff1-ce00-00000000/<hostname>@<realm*>  .    The hostname is the FQDN of the web application root or the host-header site collection root.  The realm is the GUID that represents the SharePoint tenant
iss Issuer. <IssuerID>@<realm*>. The issuer ID is obtained when you register the SPTrustedIdentityTokenIssuer. The realm is the GUID that represents the SharePoint tenant.
nbf Not before. The Unix epoch time upon which the token started being valid.
exp Expires. The Unix epoch time upon which the token expires.
nameid The identifier for the user (In our example an Active Directory SID value.
actortoken The encoded and signed inner token that represents the application.

Inner Token

aud Audience.  The value is 00000003-0000-0ff1-ce00-00000000/<hostname>@<realm*>  .    The hostname is the FQDN of the web application root or the host-header site collection root.  The realm is the GUID that represents the SharePoint tenant
iss Issuer.  <IssuerID>@<realm*>.  The issuer ID is obtained when you register the SPTrustedIdentityTokenIssuer.  The realm is the GUID that represents the SharePoint tenant.
nbf Not before. The Unix epoch time upon which the token started being valid.
exp Expires. The Unix epoch time upon which the token expires.
nameid Identifier for the app.  <Client ID>@<realm>.  The client ID uniquely identifies your app, this is provided by using AppRegNew.aspx or provided when registering an app in the Office Marketplace.

The script configures the value for all of the above, and then uses them to construct a valid JWT token that SharePoint will accept. We’ll look at this in detail now.

The PowerShell Script

I’ll attach the finished script at the bottom of this post, so don’t worry about trying to cut and paste the segments.. download the whole thing at the end!

The first thing our script does is pull in the various assemblies that we’ll be using.

##Setup Assemblies
[System.Reflection.Assembly]::LoadWithPartialName("System.Security") | out-null
[System.Reflection.Assembly]::LoadWithPartialName("mscorlib") | out-null
$identityDllPath = $PSScriptRoot + "\System.identitymodel.tokens.jwt.dll"
add-type -LiteralPath $identityDllPath

We’re using the add-type method for the System.Identitymodel.tokens.JWT.dll because this is copied from the NUGet package into the same location as the script.

##Setup the root web and test query endpoint
$rootWeb = "https://sharepointsite.company.co.uk"
$apiInterface = "/_api/search/query?querytext='sharepoint'"

##get certificate details
$privateCertPath = $PSScriptRoot + "\High-Trust-Cert.pfx"
$certPW = "UnsecureTextPassword"
$certPWSecure = ConvertTo-SecureString $certPW -AsPlainText -force

## set up the domain, clientid and realm.
$domain = "sharepointsite.company.co.uk"
$clientID = "c9725386-972e-45d6-8996-68333dae889a"
$realm = Get-SPAuthenticationRealm

##now setup the items we need to build the tokens
$aud = "00000003-0000-0ff1-ce00-000000000000/" + $domain + "@" + $realm
$iss = $clientID + "@" + $realm
$nameid = "s-1-5-21-309554102-208957183-1128231545-40032" ##This is the SID for our test account
$actorNameid = $iss
$nii = "urn:office:idp:activedirectory"
$nbf = [DateTime]::UtcNow
$exp = [DateTime]::Now.AddHours(4).ToUniversalTime()

With all the configuration in place, we can start to build the tokens.

##Now create a new X509 cert object with it.. this includes the Private key which allows us to Sign stuff!
$cert = new-object System.Security.Cryptography.X509Certificates.X509Certificate2($privateCertPath, $certPWSecure)

With the certificate in place, we can get the first object that really makes our lives easier and this is an X509SigningCredentials object. We’ll pass this into the JWT Token when we create it to automatically sign our Actor token to prove this is a valid app request.

##Setup the signing credentials from the certificate
[System.IdentityModel.Tokens.X509SigningCredentials] $signingCredentials = new-object System.IdentityModel.Tokens.X509SigningCredentials($cert,[System.IdentityModel.Tokens.SecurityAlgorithms]::RsaSha256Signature, [System.IdentityModel.Tokens.SecurityAlgorithms]::Sha256Digest)

With that done, we can build the Actor token (This is the Inner token that goes inside the main token).

##############################
##now build the Actor Token ##
##############################
[System.IdentityModel.Tokens.JwtHeader] $jwtActorHeader = new-object System.IdentityModel.Tokens.JwtHeader($signingCredentials)

Note that we pass the SigningCcredentials object into the JWTHeader, this pre-configures the header to be signed. This saved me a LOT of code as we didn’t have to do our own Base64URLencoding and then signing which was the realm stumbling block in PoSH 2.0.

$nameIdClaim = new-object System.Security.Claims.Claim("nameid",$actorNameid)
$delegateClaim = new-object System.Security.Claims.Claim("trustedfordelegation","true")
$claims = new-object 'System.Collections.Generic.List[System.Security.Claims.Claim]'
$claims.Add($nameIdClaim)
$claims.Add($delegateClaim)

[System.IdentityModel.Tokens.JwtPayload] $jwtActorPayload = New-Object System.IdentityModel.Tokens.JwtPayload($iss,$aud,$claims,$nbf,$exp)

With the Actor Header and Actor Payload created, we can now create the full ActorToken.

[System.IdentityModel.Tokens.JwtSecurityToken] $jwtActorToken  = New-Object System.IdentityModel.Tokens.JwtSecurityToken($jwtActorHeader,$jwtActorPayload)

And then use the JWTSecurityTokenHandler to write out the fully encoded token into a PoSH variable for inclusion in the outer token.

[System.IdentityModel.Tokens.JwtSecurityTokenHandler] $jwtSecurityTokenHandler = new-object System.IdentityModel.Tokens.JwtSecurityTokenHandler
$completeEncryptedActorToken = $jwtSecurityTokenHandler.WriteToken($jwtActorToken)

Now we have the actor token, we pretty much repeat the same again including the $completeEncryptedActorToken as one of the claims inside the outer JWT. This outer token is also unsigned, so we don’t use the signing credentials to create the token and instead add the header values manually.

##############################
##now build the Outer Token ##
##############################
[System.IdentityModel.Tokens.JwtHeader] $jwtOuterHeader = new-object System.IdentityModel.Tokens.JwtHeader
$jwtOuterHeader.Add("typ","JWT")
$jwtOuterHeader.Add("alg","none")

With the header complete, we can now create the payload.

$outerNameIdClaim = new-object System.Security.Claims.Claim("nameid",$nameid) ##This is the SID of the person we're impersonating
$outerNiiClaim = new-object System.Security.Claims.Claim("nii",$nii)
$outerActorClaim = new-object System.Security.Claims.Claim("actortoken",$completeEncryptedActorToken) 
$outerClaims = new-object 'System.Collections.Generic.List[System.Security.Claims.Claim]'
$outerClaims.Add($outerNameIdClaim)
$outerClaims.Add($outerNiiClaim)
$outerClaims.Add($outerActorClaim)

[System.IdentityModel.Tokens.JwtPayload] $jwtOuterPayload = New-Object System.IdentityModel.Tokens.JwtPayload($iss,$aud,$outerClaims,$nbf,$exp)

For this payload, the nameid is now that of the person that we want to impersonate, so we use the SID value from Active Directory.

##Complete the token
[System.IdentityModel.Tokens.JwtSecurityToken] $jwtOuterToken  = New-Object System.IdentityModel.Tokens.JwtSecurityToken($jwtOuterHeader,$jwtOuterPayload)

$finishedToken = $jwtSecurityTokenHandler.WriteToken($jwtOuterToken)

The token is now complete and $finishedToken contains the Base64URLEncoded value that you can see carried in the Bearer authorisation header in any Provider Hosted app connection to SharePoint. To use this, we merely need to make a web request against a known REST endpoint, using the token as our Bearer token in our headers.

$webRequestURL = $rootWeb + $apiInterface
$request = [System.Net.WebRequest]::Create($webRequestURL)
$request.Accept = "application/json;odata=verbose"
$request.Headers.Add("X-FORMS_BASED_AUTH_ACCEPTED","f")
$request.Headers.Add("X-HTTP-Method", "GET")
$request.Headers.Add("Authorization", "Bearer " + $finishedToken)
$request.Referer = “https://drupalsystem.companyname.co.uk”

At this point, one of two things will happen. Either:

  1. Your request proceeds and you can continue to read the $request response stream and then go on to validate your request.
  2. Your token is invalid, in which case you will receive a 401 access denied.

If you get the 401 problem, I highly recommend you fire up visual studio and load up Kirk Evan’s Fiddler4 extension for JWTs. This great extension lets you view the contents of the JWT. The only thing it can’t do is validate the certificate for you, so if the JWT looks right, then you need to focus on your certificate and ensure that you have the right one, with the private key.

Attached here is a text file containing the PS1 script that I used, It’s a slightly less polished version of the one that went into Live, so I will caution that it’s not production ready and issued here as a training aid really.. If you break stuff with it there’s no warranty. It’s offered under the MIT License.

I hope this post helps you if you’re looking for a way to test your app infrastructure and wish you the best of luck going forward as getting this far has been hard work!

Paul.

2 pings

  1. POKORNY | Episode 055 on how Sunrise uses the Calendar API with Pierre-Élie Fauche—Office 365 Developer Podcast

    […] Automated testing of a Provider Hosted App with PowerShell […]

  2. Office 365—monthly Dev Digest for August • PC Portal

    […] Automated testing of a Provider Hosted App with PowerShell […]

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*