Switching Apigee Management Endpoint to OAuth

on Monday, June 11, 2018

So, Apigee is updating their REST Management API to no longer accept Basic Authorization headers and instead use OAuth tokens. It’s a good move, as it adds a little more security by issuing tokens that are only valid for 30 minutes. The strength of the security is still provided through HTTPS connections and proper storage of credentials by the users/clients.

To make the update, I’ll be switching out a Basic Authentication setup with an OAuth setup. Apigee decided to go with a password grant, with the optional parameters not used. It’s a bit interesting that they went with a password grant over a client credentials grant. The client credential grant seems a lot more straight forward, and it would fit the scenario that each end user/client can directly use their credentials to create automated tooling.

By using a password grant it would imply that they couldn’t use their Apigee Server as the real OAuth server. Their OAuth server is at https://login.apigee.com, and it must be the endpoint that provides SSO protection for https://apigee.com/edge (the Management Website / Management UI). And, any users within that OAuth server must be provisioned into the REST Management API system (https://api.enterprise.apigee.com) as “Applications”. But that’s all speculation.

However they do it, converting for Basic Authentication to OAuth is pretty straight forward. The trickiest part is adding retry logic to the calls that fails because an access token has expired. We just need to add code to detect the 401 Unauthorized response, ask for a new token and then retry the call.

Basic Authentication (Before):


########### Apigee.psm1

# bump up TLS to 1.2
[System.Net.ServicePointManager]::SecurityProtocol = 
				[System.Net.SecurityProtocolType]::Tls12 + [System.Net.SecurityProtocolType]::Tls11 + [System.Net.SecurityProtocolType]::Tls;

$global:Apigee = @{}

# get username / password for management API
import-module secretserver
$secret = get-secretserversecret -filter "apigee - admin account"

$global:Apigee.username = $secret.username
$global:Apigee.password = $secret.password

$combo = $global:apigee.username + ":" + $global:apigee.password
$plaintextbytes = [system.text.encoding]::utf8.getbytes($combo)
$base64encoded = [system.convert]::tobase64string($plaintextbytes)
$basicauth = "basic $base64encoded"
$global:apigee.authheader = @{ authorization = $basicauth }

$global:Apigee.ApiUrl = "https://api.enterprise.apigee.com/v1/organizations/"

# grab functions from files (from C:\Chocolatey\chocolateyinstall\helpers\chocolateyInstaller.psm1)
Resolve-Path $root\Apigee.*.ps1 | 
	? { -not ($_.ProviderPath.Contains(".Tests.")) } |
	% { . $_.ProviderPath; }




########### Apigee.Rest.ps1

Function Invoke-ApigeeRest {
[CmdletBinding()]
Param (
	[Parameter(Mandatory = $true)]
	[string] $ApiPath,
	[ValidateSet("Default","Delete","Get","Head","Merge","Options","Patch","Post","Put","Trace")]
	[string] $Method = "Default",
	[object] $Body = $null,
	[string] $ContentType = "application/json",
	[string] $OutFile = $null
)

	if($ApiPath.StartsWith("/") -eq $false) {
		$ApiPath = "/$ApiPath"
	}
	$Uri = $global:Apigee.ApiUrl + $ApiPath
   
	if($Body -eq $null) {
		$result = Invoke-RestMethod `
					-Uri $Uri `
					-Method $Method `
					-Headers $global:Apigee.AuthHeader `
					-ContentType $ContentType `
					-OutFile $OutFile
	} else {
		$result = Invoke-RestMethod `
					-Uri $Uri `
					-Method $Method `
					-Headers $global:Apigee.AuthHeader `
					-Body $Body `
					-ContentType $ContentType `
					-OutFile $OutFile
	}
	
	return $result
}


[string[]]$funcs =
	"Invoke-ApigeeRest"

Export-ModuleMember -Function $funcs

OAuth Authentication (After):

########### Apigee.psm1


# bump up TLS to 1.2
[System.Net.ServicePointManager]::SecurityProtocol = 
				[System.Net.SecurityProtocolType]::Tls12 + [System.Net.SecurityProtocolType]::Tls11 + [System.Net.SecurityProtocolType]::Tls;

$global:Apigee = @{}

# get username / password for management API
import-module secretserver
$secret = get-secretserversecret -filter "apigee - admin account"

$global:Apigee.username = $secret.username
$global:Apigee.password = $secret.password

$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 = @{}
$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.Body = @{
                                                username = $global:Apigee.Username
                                                password = $global:Apigee.Password
                                                grant_type = "password"
                                        }
$global:Apigee.OAuthLogin.ResultObjectName = "ApigeeAccessToken"
# $global:Apigee.OAuthToken set below
# $global:Apigee.AuthHeader set in Apigee.Login.ps1

# 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







########### Apigee.Login.psm1



<#
.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 ()

    $results =	Invoke-WebRequest `
                    -Uri $global:Apigee.OAuthLogin.Url `
                    -Method $global:Apigee.OAuthLogin.Method `
                    -Headers $global:Apigee.OAuthLogin.Headers `
                    -ContentType $global:Apigee.OAuthLogin.ContentType `
                    -Body $global:Apigee.OAuthLogin.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







########### Apigee.Rest.psm1


Function Invoke-ApigeeRest {
[CmdletBinding()]
Param (
	[Parameter(Mandatory = $true)]
	[string] $ApiPath,
	[ValidateSet("Default","Delete","Get","Head","Merge","Options","Patch","Post","Put","Trace")]
	[string] $Method = "Default",
	[object] $Body = $null,
	[string] $ContentType = "application/json",
	[string] $OutFile = $null
)

	if($ApiPath.StartsWith("/") -eq $false) {
		$ApiPath = "/$ApiPath"
	}
	$Uri = $global:Apigee.ApiUrl + $ApiPath

    $attempt = 1
    $retry = $false
        
    do {
        $retry = $false

        try {

	        if($Body -eq $null) {
		        $result = Invoke-RestMethod `
					        -Uri $Uri `
					        -Method $Method `
					        -Headers $global:Apigee.AuthHeader `
					        -ContentType $ContentType `
					        -OutFile $OutFile
	        } else {
		        $result = Invoke-RestMethod `
					        -Uri $Uri `
					        -Method $Method `
					        -Headers $global:Apigee.AuthHeader `
					        -Body $Body `
					        -ContentType $ContentType `
					        -OutFile $OutFile
	        }

        } catch {

            # if the request is unauthorized, get a new access tokens & retry
            $isWebException = (Get-PsType -PSObject $_.Exception) -eq "System.Net.WebException"
            $is401Unauthorized = $_.Exception.Message -eq "The remote server returned an error: (401) Unauthorized."
            
            if($isWebException -and $is401Unauthorized) {
                Get-ApigeeAccessTokens
                
                if($attempt -lt 2) {
                    $retry = $true
                }
            } else {

                throw    # unexpected exception, so rethrow

            }
        }

        $attempt++

    } while( $retry )
	
	return $result
}


[string[]]$funcs =
	"Invoke-ApigeeRest"

Export-ModuleMember -Function $funcs


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