PingOne Platform APIs

Step 4: Exchange code for token (NO AUTH + PKCE)

POST {{authPath}}/{{envID}}/as/token

You can use the /{{envID}}/as/token endpoint to obtain an access token by presenting its authorization grant. In this activity, the token request identifies authorization_code as the grant type, and it must also provide the code value returned in the resume endpoint.

With the pkceEnforcment property enabled on the application, the token request must provide the code_verifier parameter value. This parameter is used to verify the code_challenge value submitted in the authorization request.

The token request transforms the code_verifier property value using the code_challenge_method specified in the authorize request. If the transformed code_verifier value is equal to the code_challenge value submitted in the authorize request, then the authorization server issues the token.

Usage summary

This workflow demonstrates authentication for public clients (SPAs, mobile apps) that cannot securely store secrets. Here are the key factors:

Token request

  • No Client Secret: Public clients don’t have secrets

  • Request body:

    • grant_type is authorization_code

    • code (authorization code)

    • redirect_uri

    • client_id is {{publicClientAppID}} (only client_id, no secret)

    • code_verifier is the {{pkceCodeVerifier}}

PKCE flow

  • Authorization: Send code_challenge (SHA256 hash of verifier)

  • Token: Send code_verifier (original random string)

  • Server verifies: SHA256(code_verifier) equals the code_challenge value

  • Prevents code interception attacks

Security model

  • No shared secrets to steal.

  • PKCE protects against authorization code interception.

  • Code verifier proves possession of original challenge.

  • Safe for JavaScript apps, mobile apps.

When to use

  • Single Page Applications (React, Angular, Vue)

  • Mobile apps (iOS, Android)

  • Desktop apps without secure storage

  • Any client that cannot keep secrets confidential

Important: Public clients are limited to certain grant types and should use short-lived tokens with refresh token rotation.

Headers

Content-Type      application/x-www-form-urlencoded

Body

urlencoded ( application/x-www-form-urlencoded )

Key Value

grant_type

authorization_code

code

{{publicAuthCode}}

redirect_uri

http://localhost:3000/callback

client_id

{{nativeAppID}}

code_verifier

{{pkceCodeVerifier}}

Example Request

  • cURL

  • C#

  • Go

  • HTTP

  • Java

  • jQuery

  • NodeJS

  • Python

  • PHP

  • Ruby

  • Swift

curl --location --globoff '{{authPath}}/{{envID}}/as/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code={{publicAuthCode}}' \
--data-urlencode 'redirect_uri=http://localhost:3000/callback' \
--data-urlencode 'client_id={{nativeAppID}}' \
--data-urlencode 'code_verifier={{pkceCodeVerifier}}'
var options = new RestClientOptions("{{authPath}}/{{envID}}/as/token")
{
  MaxTimeout = -1,
};
var client = new RestClient(options);
var request = new RestRequest("", Method.Post);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
request.AddParameter("grant_type", "authorization_code");
request.AddParameter("code", "{{publicAuthCode}}");
request.AddParameter("redirect_uri", "http://localhost:3000/callback");
request.AddParameter("client_id", "{{nativeAppID}}");
request.AddParameter("code_verifier", "{{pkceCodeVerifier}}");
RestResponse response = await client.ExecuteAsync(request);
Console.WriteLine(response.Content);
package main

import (
  "fmt"
  "strings"
  "net/http"
  "io"
)

func main() {

  url := "{{authPath}}/{{envID}}/as/token"
  method := "POST"

  payload := strings.NewReader("grant_type=authorization_code&code=%7B%7BpublicAuthCode%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&client_id=%7B%7BnativeAppID%7D%7D&code_verifier=%7B%7BpkceCodeVerifier%7D%7D")

  client := &http.Client {
  }
  req, err := http.NewRequest(method, url, payload)

  if err != nil {
    fmt.Println(err)
    return
  }
  req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

  res, err := client.Do(req)
  if err != nil {
    fmt.Println(err)
    return
  }
  defer res.Body.Close()

  body, err := io.ReadAll(res.Body)
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(string(body))
}
POST /{{envID}}/as/token HTTP/1.1
Host: {{authPath}}
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=%7B%7BpublicAuthCode%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&client_id=%7B%7BnativeAppID%7D%7D&code_verifier=%7B%7BpkceCodeVerifier%7D%7D
OkHttpClient client = new OkHttpClient().newBuilder()
  .build();
MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
RequestBody body = RequestBody.create(mediaType, "grant_type=authorization_code&code={{publicAuthCode}}&redirect_uri=http://localhost:3000/callback&client_id={{nativeAppID}}&code_verifier={{pkceCodeVerifier}}");
Request request = new Request.Builder()
  .url("{{authPath}}/{{envID}}/as/token")
  .method("POST", body)
  .addHeader("Content-Type", "application/x-www-form-urlencoded")
  .build();
Response response = client.newCall(request).execute();
var settings = {
  "url": "{{authPath}}/{{envID}}/as/token",
  "method": "POST",
  "timeout": 0,
  "headers": {
    "Content-Type": "application/x-www-form-urlencoded"
  },
  "data": {
    "grant_type": "authorization_code",
    "code": "{{publicAuthCode}}",
    "redirect_uri": "http://localhost:3000/callback",
    "client_id": "{{nativeAppID}}",
    "code_verifier": "{{pkceCodeVerifier}}"
  }
};

$.ajax(settings).done(function (response) {
  console.log(response);
});
var request = require('request');
var options = {
  'method': 'POST',
  'url': '{{authPath}}/{{envID}}/as/token',
  'headers': {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  form: {
    'grant_type': 'authorization_code',
    'code': '{{publicAuthCode}}',
    'redirect_uri': 'http://localhost:3000/callback',
    'client_id': '{{nativeAppID}}',
    'code_verifier': '{{pkceCodeVerifier}}'
  }
};
request(options, function (error, response) {
  if (error) throw new Error(error);
  console.log(response.body);
});
import requests

url = "{{authPath}}/{{envID}}/as/token"

payload = 'grant_type=authorization_code&code=%7B%7BpublicAuthCode%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&client_id=%7B%7BnativeAppID%7D%7D&code_verifier=%7B%7BpkceCodeVerifier%7D%7D'
headers = {
  'Content-Type': 'application/x-www-form-urlencoded'
}

response = requests.request("POST", url, headers=headers, data=payload)

print(response.text)
<?php
require_once 'HTTP/Request2.php';
$request = new HTTP_Request2();
$request->setUrl('{{authPath}}/{{envID}}/as/token');
$request->setMethod(HTTP_Request2::METHOD_POST);
$request->setConfig(array(
  'follow_redirects' => TRUE
));
$request->setHeader(array(
  'Content-Type' => 'application/x-www-form-urlencoded'
));
$request->addPostParameter(array(
  'grant_type' => 'authorization_code',
  'code' => '{{publicAuthCode}}',
  'redirect_uri' => 'http://localhost:3000/callback',
  'client_id' => '{{nativeAppID}}',
  'code_verifier' => '{{pkceCodeVerifier}}'
));
try {
  $response = $request->send();
  if ($response->getStatus() == 200) {
    echo $response->getBody();
  }
  else {
    echo 'Unexpected HTTP status: ' . $response->getStatus() . ' ' .
    $response->getReasonPhrase();
  }
}
catch(HTTP_Request2_Exception $e) {
  echo 'Error: ' . $e->getMessage();
}
require "uri"
require "net/http"

url = URI("{{authPath}}/{{envID}}/as/token")

http = Net::HTTP.new(url.host, url.port);
request = Net::HTTP::Post.new(url)
request["Content-Type"] = "application/x-www-form-urlencoded"
request.body = "grant_type=authorization_code&code=%7B%7BpublicAuthCode%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&client_id=%7B%7BnativeAppID%7D%7D&code_verifier=%7B%7BpkceCodeVerifier%7D%7D"

response = http.request(request)
puts response.read_body
let parameters = "grant_type=authorization_code&code=%7B%7BpublicAuthCode%7D%7D&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&client_id=%7B%7BnativeAppID%7D%7D&code_verifier=%7B%7BpkceCodeVerifier%7D%7D"
let postData =  parameters.data(using: .utf8)

var request = URLRequest(url: URL(string: "{{authPath}}/{{envID}}/as/token")!,timeoutInterval: Double.infinity)
request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

request.httpMethod = "POST"
request.httpBody = postData

let task = URLSession.shared.dataTask(with: request) { data, response, error in
  guard let data = data else {
    print(String(describing: error))
    return
  }
  print(String(data: data, encoding: .utf8)!)
}

task.resume()

Example Response

200 OK

{
    "access_token": "eyJraWQiOiI2YzkyNjVjMC1iNWJ",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "openid",
    "id_token": "eyJraWQiOiI2YzkyNjVjMC1iNWJm"
}