mbromb
mbromb

Reputation: 61

Powershell Exchange EWS script authentication using Oauth unable to use a save password hash file

Normally for scheduled scripts I save a hash file to disk for the credentials to be used by the script as follows:

$Credential = Get-Credential [email protected] 
$Credential.Password | ConvertFrom-SecureString | Set-Content "C:\admin.pwd" $Username = "[email protected]" 
$Password = Get-Content "C:\admin.pwd" -ErrorAction stop | ConvertTo-SecureString
$Credential = New-Object System.Management.Automation.PSCredential($Username,$Password) 

The following Oath token request works if the password element in the Body is entered in plain text, but it is not working if I use the variable $Credential.Password . Is there a way to get this to work, or secure the password otherwise?

The Error that the below token request generatedes:

Error: Invoke-RestMethod : {"error":"invalid_grant","error_description":"AADSTS50126: Error validating credentials due to invalid username or password..."error_uri":"login.microsoftonline.com/error?code=50126"}

## Request an access token

# Define AppId, secret and scope, your tenant name and endpoint URL
$AppId = 'AppIdHere'
$AppSecret = 'AppSecretHere'
$Scope = "https://outlook.office365.com/.default"
$TenantName = "Domain.onmicrosoft.com"
$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

# Add System.Web for urlencode
Add-Type -AssemblyName System.Web

# Create body
$Body = @{
    client_id = $AppId
    client_secret = $AppSecret
    scope = $Scope
    grant_type = 'password'
    username = '[email protected]'
    password = $Credential.Password
}

# Splat the parameters for Invoke-Restmethod for cleaner code
$PostSplat = @{
    ContentType = 'application/x-www-form-urlencoded'
    Method = 'POST'

    # Create string by joining bodylist with '&'
    Body = $Body
    Uri = $Url
}

# Request the token for user!
$Request = Invoke-RestMethod @PostSplat

$Request.access_token

##########

===========================

Updated script based on answer from thepip3r, and Microsoft support:

Password and secret are passed in plain text on the wire, but are not exposed in the script, and have a degree of security saved as hash files

Adjusted to not save the password or secret into variables to increase security from attacks that can gain access to memory (MS support recommended)

Option to use a certificate for the Azure Registered App rather than an App Secret, to increase security on the wire

Alternative option is to use "Azure Automation", which allows to run scripts from within O365, which should be much more secure. Another possible alternative may be Azure Functions.

# One time AppID\Secret hash save to file:
## $AppCredential = Get-Credential 'AppIdHere'
## $AppCredential.Password | ConvertFrom-SecureString | Set-Content "C:\App.pwd" 

# One time Admin hash save to file:
## $Credential = Get-Credential [email protected]
## $Credential.Password | ConvertFrom-SecureString | Set-Content "C:\admin.pwd" 

$AppId = 'AppIdHere'
$AppS = Get-Content "C:\App.pwd"  | ConvertTo-SecureString
$AppCredential = New-Object System.Management.Automation.PSCredential($AppId,$AppS)

$Username = "[email protected]"
$Password = Get-Content "C:\admin.pwd" | ConvertTo-SecureString
$Credential = New-Object System.Management.Automation.PSCredential($Username,$Password)        

### Request an access token ###

$Scope = "https://outlook.office365.com/.default"
$TenantName = "usablelife.onmicrosoft.com"
$Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

# Add System.Web for urlencode
Add-Type -AssemblyName System.Web

# Request the token!
$Request = Invoke-RestMethod -Body @{
    client_id = $AppId
    client_secret = $AppCredential.GetNetworkCredential().Password
    scope = $Scope
    grant_type = 'password'
    username = $Username 
    password = $Credential.GetNetworkCredential().Password
} `
-ContentType 'application/x-www-form-urlencoded' `
-Method 'POST' `
-Uri "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"

Upvotes: 0

Views: 544

Answers (1)

thepip3r
thepip3r

Reputation: 2935

So... I'm going to provide this as an answer because I want people to understand this being an example the problem with storing a password in a file (even as a ciphertext string value) with Get-Credential.

@mbromb, this will give you a way to test whether or not the value you're retrieving is the proper value:

on your $Credential object (last line), run: $Credential.GetNetworkCredential().Password

This will be the PLAINTEXT value of whatever you put into the prompts with Get-Credential initially. So, you can verify if after getting it initially, writing it to a file, reading it back in, and converting it to a securestring object worked as intended.

To try and draw a more straight-line to this problem: if I find your 'admin.pwd' file, it's extremely trivial to produce the plaintext from it.

Caveat: You can secure this value by supplying a protected key for this encryption process using either the -Key or -SecureKey properties on the ConvertTo/From-SecureString cmdlets. Key takes a byte array (preferably cryptographically random with sufficient entropy for your needs) and SecureKey accepts a string (password) and generates the byte array from your password.

Caveat-to-the-caveat: If you're already trying to store the password to a file, password protecting the stored-password probably isn't the right answer...

Upvotes: 2

Related Questions