Not too long ago Apigee updated their documentation to show that Basic Authentication was going to be deprecated on their Management API. This wasn’t really a big deal and it isn’t very difficult to implement an OAuth 2.0 machine-to-machine (grant_type=password) authentication system. Apigee has documentation on how to use their updated version of curl (ie. acurl) to make the calls. But, if you read through a generic explanation of using OAuth it’s pretty straight forward.
But, what about using MFA One Time Password Token’s (OTP) with OAuth authentication? Apigee supports the usage of Google Authenticator to do OTP tokens when signing in through the portal. And … much to my surprise … they also support the OTP tokens in their Management API OAuth login. They call the parameter, mfa_token.
This will sound crazy, but we wanted to setup MFA on an account that is used by a bot/script. Since the bot is only run from a secure location, and the username/password are already securely stored outside of the bot there is really no reason to add MFA to the account login process. It already meets all the criteria for being securely managed. But, on the other hand, why not see if it’s possible?
The only thing left that needed to be figured out was how to generate the One Time Password used by the mfa_token parameter. And, the internet had already done that! (Thank You James Nelson!) All that was left to do was find the Shared Secret Key that the OTP function needed.
Luckily I work with someone knowledgeable on the subject and they pointed out not only that the OTP algorithm that Google Authenticator uses is available on the internet but that Apigee MFA sign-up screen had the Shared Secret Key available on the page. (Thank You Kevin Wu!)
When setting up Google Authenticator in Apigeee, click on the Unable to Scan Barcode? link
Which reveals the OTP Shared Secret:
From there, you just need a little Powershell to tie it all together:
- Apigee.OTP.ps1: A conversion of James Nelson’s Get-OTP.ps1 script into a function.
- Apigee.Login.ps1: An implementation of Apigee Management API OAuth Login
- Apigee.psm1: A script putting the final pieces in place (like the username/password/sharedsecret).
# This file shouldn't be run on it's own. It should be loaded using the Apigee Module. | |
<# | |
.SYNOPSIS | |
Implementation of the Time-based One-time Password Algorithm used by Google Authenticator. | |
.DESCRIPTION | |
As described in http://tools.ietf.org/id/draft-mraihi-totp-timebased-06.html, the script generates a one-time password based on a shared secret key and time value. | |
This script generates output identical to that of the Google Authenticator application, but is NOT INTENDED FOR PRODUCTION USE as no effort has been made to code securely or protect the key. For demonstration-use only. | |
Script code is essentially a transation of a javascript implementation found at http://jsfiddle.net/russau/uRCTk/ | |
Output is a PSObject that includes the generated OTP, the values of intermediate calculations, and a URL leading to a QR code that can be used to generate a corresponding OTP in Google Authenticator applications. | |
The generated QR code contains a URL that takes the format "otpauth://totp/<email_address_here>?secret=<secret_here>", for example: otpauth://totp/tester@test.com?secret=JBSWY3DPEHPK3PXP | |
The generated OTP is (obviously) time-based, so this script outptu will only match Google Authenticator output if the clocks on both systems are (nearly) in sync. | |
The acceptable alphabet of a base32 string is ABCDEFGHIJKLMNOPQRSTUVWXYZ234567. | |
Virtually no parm checking is done in this script. Caveat Emptor. | |
.PARAMETER sharedSecretKey | |
A random, base32 string shared by both the challenge and reponse side of the autheticating pair. This script mandates a string length of 16. | |
.EXAMPLE | |
.\Get-OTP.ps1 -sharedSecret "JBSWY3DPEHPK3PXP" | Select SharedSecret, Key, Time, HMAC, URL, OTP | |
.NOTES | |
FileName: Get-OTP.ps1 | |
Author: Jim Nelson nelsondev1 | |
#> | |
Function Get-ApigeeOTP { | |
[CmdletBinding()] | |
param | |
( | |
[Parameter(Mandatory=$true,ValueFromPipeline=$true)] | |
[ValidateLength(16,16)] | |
[string] $SharedSecret, | |
[Parameter(Mandatory=$true,ValueFromPipeline=$true)] | |
[string] $Email | |
) | |
#------------------------------------------------------------------------------ | |
#------------------------------------------------------------------------------ | |
# Converts the supplied Int64 value to hexadecimal. | |
#------------------------------------------------------------------------------ | |
function Convert-DecimalToHex($in) | |
{ | |
return ([String]("{0:x}" -f [Int64]$in)).ToUpper() | |
} | |
#------------------------------------------------------------------------------ | |
#------------------------------------------------------------------------------ | |
# Converts the supplied hexadecimal value Int64. | |
#------------------------------------------------------------------------------ | |
function Convert-HexToDecimal($in) | |
{ | |
return [Convert]::ToInt64($in,16) | |
} | |
#------------------------------------------------------------------------------ | |
#------------------------------------------------------------------------------ | |
# Converts the supplied hexadecimal string to a byte array. | |
#------------------------------------------------------------------------------ | |
function Convert-HexStringToByteArray($String) | |
{ | |
return $String -split '([A-F0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}} | |
} | |
#------------------------------------------------------------------------------ | |
#------------------------------------------------------------------------------ | |
# Converts the supplied base32 string to a hexadecimal string | |
#------------------------------------------------------------------------------ | |
function Convert-Base32ToHex([String]$base32) | |
{ | |
$base32 = $base32.ToUpper() | |
$base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" | |
$bits = "" | |
$hex = "" | |
# convert char-by-char of input into 5-bit chunks of binary | |
foreach ($char in $base32.ToCharArray()) | |
{ | |
$tmp = $base32chars.IndexOf($char) | |
$bits = $bits + (([Convert]::ToString($tmp,2))).PadLeft(5,"0") | |
} | |
# leftpad bits with 0 until length is a multiple of 4 | |
while ($bits.Length % 4 -ne 0) | |
{ | |
$bits = "0" + $bits | |
} | |
# convert binary chunks of 4 into hex | |
for (($tmp = $bits.Length -4); $tmp -ge 0; $tmp = $tmp - 4) | |
{ | |
$chunk = $bits.Substring($tmp, 4); | |
$dec = [Convert]::ToInt32($chunk,2) | |
$h = Convert-DecimalToHex $dec | |
$hex = $h + $hex | |
} | |
return $hex | |
} | |
#------------------------------------------------------------------------------ | |
#------------------------------------------------------------------------------ | |
# Get the currentUnix epoch (div 30) in hex, left-padded with 0 to 16 chars | |
#------------------------------------------------------------------------------ | |
function Get-EpochHex() | |
{ | |
# this line from http://shafiqissani.wordpress.com/2010/09/30/how-to-get-the-current-epoch-time-unix-timestamp/ | |
$unixEpoch = ([DateTime]::Now.ToUniversalTime().Ticks - 621355968000000000) / 10000000 | |
$h = Convert-DecimalToHex ([Math]::Floor($unixEpoch / 30)) | |
return $h.PadLeft(16,"0") | |
} | |
#------------------------------------------------------------------------------ | |
#------------------------------------------------------------------------------ | |
# Get the HMAC signature for the supplied key and time values. | |
#------------------------------------------------------------------------------ | |
function Get-HMAC($key, $time) | |
{ | |
$hashAlgorithm = New-Object System.Security.Cryptography.HMACSHA1 | |
$hashAlgorithm.key = Convert-HexStringToByteArray $key | |
$signature = $hashAlgorithm.ComputeHash((Convert-HexStringToByteArray $time)) | |
$result = [string]::join("", ($signature | % {([int]$_).toString('x2')})) | |
$result = $result.ToUpper() | |
return $result | |
} | |
#------------------------------------------------------------------------------ | |
#------------------------------------------------------------------------------ | |
# Get the OTP based on the supplied HMAC | |
#------------------------------------------------------------------------------ | |
function Get-OTPFromHMAC($hmac) | |
{ | |
$offset = Convert-HexToDecimal($hmac.Substring($hmac.Length -1)) | |
$p1 = Convert-HexToDecimal($hmac.Substring($offset*2,8)) | |
$p2 = Convert-HexToDecimal("7fffffff") | |
[string]$otp = $p1 -band $p2 | |
$otp = $otp.Substring($otp.Length - 6, 6) | |
return $otp | |
} | |
# ------------------------------------------------------------------------------------------------------- | |
# ------------------------------------------------------------------------------------------------------- | |
# ------------------------------------------------------------------------------------------------------- | |
# MAIN PROGRAM | |
# ------------------------------------------------------------------------------------------------------- | |
# ------------------------------------------------------------------------------------------------------- | |
$params = @{ | |
"SharedSecret" = ""; | |
"Key" = ""; | |
"Time" = ""; | |
"HMAC" = ""; | |
"OTP" = ""; | |
"URL" = ""; | |
} | |
$reportObject = New-Object PSObject -Property $params | |
# google can generate a QR code of the secret for their authenticator app at this url... | |
$url = ('https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=200x200&chld=M|0&cht=qr&chl=otpauth://totp/' + $Email + '%3Fsecret%3D' + $SharedSecret) | |
$key = Convert-Base32ToHex $sharedSecret | |
$time = Get-EpochHex | |
$hmac = Get-HMAC $key $time | |
$otp = Get-OTPFromHMAC $hmac | |
$reportObject.SharedSecret = $sharedSecret | |
$reportObject.Key = $key | |
$reportObject.Time = $time | |
$reportObject.HMAC = $hmac | |
$reportObject.OTP = $otp | |
$reportObject.URL = $url | |
return $reportObject | |
} | |
[string[]]$funcs = | |
"Get-ApigeeOTP" | |
Export-ModuleMember -Function $funcs |
# This file shouldn't be run on it's own. It should be loaded using the Apigee Module. | |
<# | |
.SYNOPSIS | |
Makes a call to the Apigee OAuth login endpoint and gets access tokens to use. | |
This should be used internally by the Apigee module. But, it shouldn't be needed by | |
the developer. | |
.EXAMPLE | |
$result = Get-ApigeeAccessTokens | |
#> | |
Function Get-ApigeeAccessTokens { | |
[CmdletBinding()] | |
[OutputType([PSCustomObject])] | |
Param () | |
$otp = Get-ApigeeOTP ` | |
-SharedSecret $global:Apigee.OAuthLogin.OTPSharedSecret ` | |
-Email $global:Apigee.OAuthLogin.Username | |
$body = @{ | |
username = $global:Apigee.OAuthLogin.Username | |
password = $global:Apigee.OAuthLogin.Password | |
mfa_token = $otp.OTP | |
grant_type = "password" | |
} | |
$results = Invoke-WebRequest ` | |
-Uri $global:Apigee.OAuthLogin.Url ` | |
-Method $global:Apigee.OAuthLogin.Method ` | |
-Headers $global:Apigee.OAuthLogin.Headers ` | |
-ContentType $global:Apigee.OAuthLogin.ContentType ` | |
-Body $body | |
if($results.StatusCode -ne 200) { | |
$resultsAsString = $results | Out-String | |
throw "Authentication with Apigee's OAuth Failed. `r`n`r`nFull Response Object:`r`n$resultsAsString" | |
} | |
$resultsObj = ConvertFrom-Json -InputObject $results.Content | |
$resultsObj = Add-PsType -PSObject $resultsObj -PsType $global:Apigee.OAuthLogin.ResultObjectName | |
Set-ApigeeAuthHeader -Authorization $resultsObj.access_token | |
return $resultsObj | |
} | |
<# | |
.SYNOPSIS | |
Sets $global:Apigee.AuthHeader @{ Authorization = "value passed in" } | |
This is used to authenticate all calls to the Apigee REST Management endpoints. | |
.EXAMPLE | |
Set-ApigeeAuthHeader -Authorization "Bearer ..." | |
#> | |
Function Set-ApigeeAuthHeader { | |
[CmdletBinding()] | |
[OutputType([PSCustomObject])] | |
Param ( | |
[Parameter(Mandatory = $true)] | |
$Authorization | |
) | |
$bearerAuth = "Bearer $Authorization" | |
$global:Apigee.AuthHeader = @{ Authorization = $bearerAuth } | |
} | |
[string[]]$funcs = | |
"Get-ApigeeAccessTokens", "Set-ApigeeAuthHeader" | |
Export-ModuleMember -Function $funcs |
if($global:Apigee -eq $null) { | |
$global:Apigee = @{} | |
$global:Apigee.ApiUrl = "https://api.enterprise.apigee.com/v1/organizations/" | |
# Use OAuth for access credentials. All public info here: | |
# https://docs.apigee.com/api-platform/system-administration/using-oauth2-security-apigee-edge-management-api | |
$global:Apigee.OAuthLogin = @{} | |
# get username / password for management API | |
$global:Apigee.OAuthLogin.Username = "store" | |
$global:Apigee.OAuthLogin.Password = "these" | |
$global:Apigee.OAuthLogin.OTPSharedSecret = "safely" | |
$global:Apigee.OAuthLogin.Method = "POST" | |
$global:Apigee.OAuthLogin.Url = "https://login.apigee.com/oauth/token" | |
$global:Apigee.OAuthLogin.ContentType = "application/x-www-form-urlencoded" | |
$global:Apigee.OAuthLogin.Headers = @{ | |
Accept = "application/json;charset=utf-8" | |
Authorization = "Basic ZWRnZWNsaTplZGdlY2xpc2VjcmV0" | |
} | |
$global:Apigee.OAuthLogin.ResultObjectName = "ApigeeAccessToken" | |
} | |
# grab functions from files (from C:\Chocolatey\chocolateyinstall\helpers\chocolateyInstaller.psm1) | |
Resolve-Path $root\Apigee.*.ps1 | | |
? { -not ($_.ProviderPath.Contains(".Tests.")) } | | |
% { . $_.ProviderPath; } | |
# get authorization token | |
$global:Apigee.OAuthToken = Get-ApigeeAccessTokens |
0 comments:
Post a Comment