question

brian-scheele avatar image
brian-scheele asked ·

PowerShell - Invoke-RestMethod for authentication

I have been programming in PowerShell for years, but I am pretty new at dealing with APIs. I have written code to deal with Meraki, where authentication just uses an API key. For RingCentral, I am a bit lost on making this work with OAUTH.

The program I am writing needs to be in PowerShell so I can integrate with AD and Exchange.

I have functioning code that I wrote to get data from RingCentral, but that only works if I first go to https://developers.ringcentral.com/api-reference and authorize my Sandbox or Production environment. I currently have a Bearer value hard-coded for now while I test, but will get it out of the code later.

It seems I may be stuck with username/password -type authentication and not something more-preferred that does not require me to have credentials stored somewhere.

In PowerShell, some basic code to get data looks like this:

#IDs, secrets, etc. are fake values below but look similar.

$ProductionClientID = "Q9GTXil5ZwqJhgzS7pBb5X"
$ProductionClientSecret = "IpUdDFj6KVFM7cYSJPXO4cK1i7eWdwvSQGYYofWLl5Oc"

$api    = @{"endpoint" = 'https://platform.ringcentral.com/restapi/v1.0'}
$header = @{"accept" = 'application/json'
            "authorization" = "Bearer 2piE4Dxnz3fGH2T1GBSE2m2D8ewFXOyxOYBN3K3ygsyr4OQ6vVnrnDGZxNOwxCZm7sIiuQipTds6NYm2kIp6cvjJIgNuYcM09NXil3nxKVOZMbJmHbH98EWkdFHNrq0R0oarLoK4OomxFeJdVgPgE5SxwzdDVQmSGgHyq0d66pMSfJ8nPLDkjumFrfxDLIfr3lpCf4ffrT5AV9JetoWCiZG2qvU35x4VsX66aitP8knPLe9aXuGlcXnEX5fLuUl9FOA9NCofZHKBKy3YKJ6EOrVXW8JITPbNml5zLJIHMsY2yvEWnfqyyU513FNcvXQh66P52AY7sNjMZfIiT58Ni7iw7qvlMzlLlHdNlqFeH1boZF"}

#This is just a sample Bearer value - not the actual one that I have hard-coded

Function Get-RingCentralAccount {
    $api.url = "/account/~"
    $uri = $api.endpoint + $api.url
    $request = Invoke-RestMethod -Method GET -Uri $uri -Headers $header
    return $request
}

Function Get-RingCentralCompanyPhoneNumbers { 
    $api.url = "/account/~/phone-number?page=1&perPage=1000"  
    $uri = $api.endpoint + $api.url
    $request = Invoke-RestMethod -Method GET -Uri $uri -Headers $header
    return $request
}


The problem I have is that there is cURL example to go by to figure out how the heck I am going to write this in PowerShell.

I am also not entirely sure if I am going to ultimately end up with the same type of header for when I run my functions. Maybe there is no Bearer value and instead there is something else I'll end up using because of the type of authentication I will have to use.

For those who have developed their app using Password Flow, can you provide examples and explain how your application keeps "logged in" to continue to interact with RingCentral while it is in use?

https://developers.ringcentral.com/guide/authentication/password-flow

This is my attempted function for authenticating, but I get errors:

Function Get-RingCentralAuthenticate { 
    $AuthHeader = @{"Content-Type" = 'application/x-www-form-urlencoded'
                    "Authorization" = 'Basic IpUdDFj6KVFM7cYSJPXO4cK1i7eWdwvSQGYYofWLl5Oc' }
    $AuthAPI    = @{"endpoint"     = 'https://platform.ringcentral.com/restapi'
                    "url"          = '/oauth/authorize'}
    $AuthBody   = @{"grant_type"   = 'password'
                    "username"     = '17175551212'
                    "password"     = 'SuperSecretPassword'
                    #"audience" = 'The name of my API'
                    #"scope" = 'read:sample'
                    "response_type" = 'code'
                    "client_id" = 'Q9GTXil5ZwqJhgzS7pBb5X'
                    "client_secret" = 'IpUdDFj6KVFM7cYSJPXO4cK1i7eWdwvSQGYYofWLl5Oc'}
    $uri = $AuthAPI.endpoint + $AuthAPI.url
    $request = Invoke-RestMethod -Method POST -Uri $uri -Headers $AuthHeader -Body $AuthBody 
    return $request
}

For the function, Get-RingCentralAuthenticate, originally I did not have the "Authorization" line in the header, or audience, scope, response_type, client_id, or client_secret in the body, and I get this error, which to me indicates I am missing the response type.

Invoke-RestMethod: {
  "error" : "unsupported_response_type",
  "errors" : [ {
    "errorCode" : "OAU-152",
    "message" : "Unsupported response type: ",
    "parameters" : [ {
      "parameterName" : "response_type",
      "parameterValue" : ""
    } ]
  } ],
  "error_description" : "Unsupported response type: "
}

To resolve the above error, I tried adding the response_type, trying "code" and "token" - not sure if there are other possible values. I get this error:

Invoke-RestMethod: {
  "error" : "invalid_request",
  "errors" : [ {
    "errorCode" : "OAU-155",
    "message" : "Client id is undefined"
  } ],
  "error_description" : "Client id is undefined"
}

So, now I need a client ID it seems.

At this point I don't know if I am going in a wrong direction - I am now pretty much stabbing in the dark until I hit something.

If I use the client_id value, I am now told I don't have a redirect uri, so now I am stuck until I either figure that out, or find a different rabbit hole. Redirect uri I thought was not part of Password Flow.

Invoke-RestMethod: {
  "error" : "invalid_client",
  "errors" : [ {
    "errorCode" : "OAU-113",
    "message" : "No redirect uri is registered for the client"
  } ],
  "error_description" : "No redirect uri is registered for the client"
}


------------------------------------------------------


After more monkeying around...


In the app settings (in the RingCentral app website), I created a fake oauth redirect uri: myapp://redirect

No idea if that is the right thing to do or if it can work. I found this on some site that tries to explain oauth and how it is used. It did get rid of my redirect errors.

I think I need to be using /oauth/token based on what I have seen by example on several non-RingCentral sites.

So, currently I am running my code like this. Note that I currently have client_id and client_secret commented out. In my current state, I get the same authentication error whether those are commented or not. I think I may just have a bad password value, and I have no idea how to look it up - I thought I used the same password I use when I log into RingCentral with my normal admin account, and since I don't have an extension 101 in RingCentral, I have no way to reset its password. I did try my normal extension instead of 101 - no help. If I omit username or password, it does tell me that I need those specified.

  
                 
  1. #IDs, secrets, etc. are fake values below but look similar to actual values.
  2.  
  3. Function Get-RingCentralAuthenticate {
  4. $AuthHeader = @{"Content-Type" = 'application/x-www-form-urlencoded' }
  5. $AuthAPI = @{"endpoint" = 'https://platform.ringcentral.com/restapi'
  6. "url" = '/oauth/token'}
  7. $AuthBody = @{"grant_type" = 'password'
  8. "username" = '17175551212'
  9. "extension" = '101'
  10. "password" = 'My admin login password'
  11. #"client_id" = 'Q9GTXil5ZwqJhgzS7pBb5X'
  12. #"client_secret" = 'IpUdDFj6KVFM7cYSJPXO4cK1i7eWdwvSQGYYofWLl5Oc'}
  13. $uri = $AuthAPI.endpoint + $AuthAPI.url
  14. $request = Invoke-RestMethod -Method POST -Uri $uri -Headers $AuthHeader -Body $AuthBody
  15. return $request
  16. }

Error:

  
                 
  1. Invoke-RestMethod:
  2. "error" : "invalid_client",
  3. "errors" : [ {
  4. "errorCode" : "OAU-123",
  5. "message" : "Client authentication is required"
  6. } ],
  7. "error_description" : "Client authentication is required"

I really think I just have bad credentials here, but invalid_client is a pretty crappy response if that is the actual problem. I tried bad credentials, too, with the same results.

authenticationpowershell
1587067158790.png (30.4 KiB)
1 |1000 characters needed characters left characters exceeded

Up to 8 attachments (including images) can be used with a maximum of 1.0 MiB each and 10.0 MiB total.

brian-scheele avatar image
brian-scheele answered ·

So, with the help of Phong Vu from RingCentral, I was able to finally get this figured out.

Below is a working PowerShell script with Password Authentication and a couple of basic functions.

There is a bit of commenting in here, both for my benefit and the benefit of anyone who finds this page, as well as anyone internal that uses the script.

#Call this function with a dot-space in front of the ps1 filename.
#Example: . RingCentralAPI.ps1
#You will be prompted for username, password, Client ID, Client Secret.
#Enter the values, and a Bearer value will be stored for use in other functions in this script.
#This is an early version of the code and has nothing to keep the session alive after it expires.
#Script contains no error handling and is currently proof of concept before developing for internal use.

<#  RingCentral groups their APIs into usage plans.
    
    Heavy: 10 requests per 60 seconds with 60-second Penalty Interval
    Medium: 40 requests per 60 seconds with 60-second Penalty Interval
    Light: 50 requests per 60 seconds with 60-second Penalty Interval
    Auth: 5 requests per 60 seconds with 60-second Penalty Interval

    More info: https://developers.ringcentral.com/api-reference/Usage-Plan-Groups
#>

<#Suggested Function Usage:
    Most of the functions return a "records" attribute, which usually contains the most useful data.
    
    Sample usage: $RCvariable = (Get-RingCentralInfo).records
    
    Each function will list as comments the attributes returned in a format that can be copied into a select-object statement.
    The output data is heterogeneous.  Not all records output all attributes.  
    You may want to pipe into a select-object statement and specify the attributes needed.

#>

Function Get-RingCentralAuthentication { #Auth: 5 requests per 60 seconds with 60-second Penalty Interval
<#  Prompt for credentials.  KeePass is helpful for filling in data.
    KeePass entry that can be helpful for this script would be set up with the following settings:
        User name: The user's DID
        Password:  The user's password
        Advanced tab - add these String Fields:
            ClientID with value equal to the app's Client ID
            ClientSecret with the value equal to the app's Client Secret
        Auto-Type tab - override the default sequence with this sequence:
#>
    $SessionUser            = Read-Host "User"          #Sample input: 17175551212
    $SessionPassword        = Read-Host "Password"      #Sample input: Password123
    $ClientID               = Read-Host "Client ID"     #Sample input: Y7TwATmNwJoyiLKfvvyxz7
    $ClientSecret           = Read-Host "Client Secret" #Sample input: NLpmWHRCgcwiJB5Awf33Ro6WeXvoFSRKU-7mMSxf3Gwb

    #Convert Client ID and Secret into Base64 string that is used in the initial authentication: 
    $ClientCredsPlainText   = $ClientID + ":" + $ClientSecret
    $ClientCredsBase64      = [System.Convert]::ToBase64String(
                              [System.Text.Encoding]::UTF8.GetBytes($ClientCredsPlainText))

    $AuthHeader = @{"Content-Type"  = 'application/x-www-form-urlencoded'
                    "accept"        = 'application/json'
                    "authorization" = "Basic $ClientCredsBase64" }
    $AuthAPI    = @{"endpoint"      = 'https://platform.ringcentral.com/restapi'
                    "url"           = '/oauth/token' }
    $AuthBody   = @{"grant_type"    = 'password'
                    "username"      = $SessionUser
                    "password"      = $SessionPassword }

    $uri        = $AuthAPI.endpoint + $AuthAPI.url
    $request    = Invoke-RestMethod -Method POST -Uri $uri -Headers $AuthHeader -Body $AuthBody 

    return $request

}

Function Get-RingCentralAccount { #Light: 50 requests per 60 seconds with 60-second Penalty Interval
    <#Notes:

    Suggested Usage: $RCAccountID = Get-RingCentralAccount | Select-Object -ExpandProperty id
    
    Attributes Returned:
        Primary:    id
        Other:      uri
                    serviceInfo         @{uri,brand,servicePlan,billingPlan}
                    operator            @{uri,id,extensionNumber}
                    mainNumber
                    status
                    signupInfo          @{tosAccepted,marketingAccepted}
                    setupWizardState
                    regionalSettings    @{timezone,homeCountry,language,greetingLanguage,formattingLocale,timeFormat}
                    federated
                    bsid
                    limits              @{freeSoftPhoneLinesPerExtension,meetingSize,maxMonitoredExtensionsPerUser,maxExtensionNumberLength,cloudRecordingStorage}  
    #>
    $api.url = "/account/~"
    $uri = $api.endpoint + $api.url
    $request = Invoke-RestMethod -Method GET -Uri $uri -Headers $header
    return $request
}

Function Get-RingCentralCompanyPhoneNumbers { #Heavy: 10 requests per 60 seconds with 60-second Penalty Interval
    <#Notes:
    
    Suggested Usage: $RCCompanyPhoneNumbers = (Get-RingCentralCompanyPhoneNumbers).records

    Attributes Returned:
        Primary:    records                 @{@{uri,id,country,extension,label,location,paymentType,phoneNumber,status,type,usageType,temporaryNumber,contactCenterProvider,vanityPattern }}
        Other:      paging                  @{page; totalPages; perPage; totalElements; pageStart; pageEnd}
                    navigation              @{firstPage; lastPage}

        records:    .country                @{id,uri,name,isoCode,callingCode}  
                    .extension              @{id,uri,extensionNumber,partnerID}
                    .temporaryNumber        @{id,phoneNumber}
                    .contactCenterProvider  @{id,name}
    #>

    $api.url = "/account/~/phone-number?page=1&perPage=1000"  #Adjust if we reach 1000 numbers
    $uri = $api.endpoint + $api.url
    $request = Invoke-RestMethod -Method GET -Uri $uri -Headers $header
    return $request
}

Function Get-RingCentralCompanyPhoneNumbers { #Heavy: 10 requests per 60 seconds with 60-second Penalty Interval
    <#Notes:
    
    Suggested Usage: $RCCompanyPhoneNumbers = (Get-RingCentralCompanyPhoneNumbers).records

    Attributes Returned:
        Primary:    records                 @{@{uri,id,country,extension,label,location,paymentType,phoneNumber,status,type,usageType,temporaryNumber,contactCenterProvider,vanityPattern }}
        Other:      paging                  @{page; totalPages; perPage; totalElements; pageStart; pageEnd}
                    navigation              @{firstPage; lastPage}

        records:    .country                @{id,uri,name,isoCode,callingCode}  
                    .extension              @{id,uri,extensionNumber,partnerID}
                    .temporaryNumber        @{id,phoneNumber}
                    .contactCenterProvider  @{id,name}
    #>

    $api.url = "/account/~/phone-number?page=1&perPage=1000"  #Adjust if we reach 1000 numbers
    $uri = $api.endpoint + $api.url
    $request = Invoke-RestMethod -Method GET -Uri $uri -Headers $header
    return $request
}

$Token = Get-RingCentralAuthentication
$Bearer = $Token.access_token

$Header = @{"accept" = 'application/json'
            "authorization" = "Bearer $Bearer"
            }
$API    = @{"endpoint" = 'https://platform.ringcentral.com/restapi/v1.0'}

#Test that this is actually working...
(Get-RingCentralCompanyPhoneNumbers).records


Share
1 |1000 characters needed characters left characters exceeded

Up to 8 attachments (including images) can be used with a maximum of 1.0 MiB each and 10.0 MiB total.

Phong Vu avatar image
Phong Vu answered ·

This is great! Thanks for sharing @brian-scheele

Share
1 |1000 characters needed characters left characters exceeded

Up to 8 attachments (including images) can be used with a maximum of 1.0 MiB each and 10.0 MiB total.

Write an Answer

Hint: Notify or tag a user in this post by typing @username.

Up to 10 attachments (including images) can be used with a maximum of 1.0 MiB each and 10.0 MiB total.