Apigee REST Management API with MFA

on Monday, August 20, 2018

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

image

Which reveals the OTP Shared Secret:

image

From there, you just need a little Powershell to tie it all together:

# 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
view raw Apigee.OTP.ps1 hosted with ❤ by GitHub
# 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
view raw Apigee.psm1 hosted with ❤ by GitHub

0 comments:

Post a Comment


Creative Commons License
This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.