Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: title case for section headers instead of all caps

...

Excerpt
hiddentrue

Guide for Projector Citizen Integrators

...


Some Implementation Notes

A Note about Access Tokens

...

Code Block
languagec#
titlePKCE Sample Utility Code in C# - feel free to copy or use as model in your client.
linenumberstrue
collapsetrue
using System;
using System.Text;
using System.Security.Cryptography;
using System.Linq;
					
public class Program
{
	public static void Main()
	{
		foreach(string codeChallengeMethod in new [] { "plain", "S256" })
		{
			string codeVerifier = PkceGenerateCodeVerifier();
			string codeChallenge = PkceGetCodeChallenge(codeVerifier, codeChallengeMethod);
			Console.WriteLine("Client-Derived " + codeChallengeMethod + " Code Challenge sent to server as part of authentication code request is: " + codeChallenge);
			Console.WriteLine("On receipt of authentication code request, server determines that code challenge is " + (PkceCodeChallengeIsValid(codeChallenge, codeChallengeMethod) ? string.Empty : "in") + "valid.");
			
			foreach (string sentCodeVerifier in new [] { codeVerifier, "ILLEGAL-CODE-VERIFIER-SIMULATING-ATTEMPT-TO-CRACK-THE-SYSTEM" })
			{
				Console.WriteLine("On token aquisition request, Client-generated " + codeChallengeMethod + " code verifier sent to server is: " + sentCodeVerifier);
				Console.WriteLine("Based on PKCE vervication, server does " + (PkceVerify(codeChallenge, sentCodeVerifier, codeChallengeMethod) ? string.Empty : "NOT ") + "grant access token to client.");
			}

			Console.WriteLine();
		}
	}
	
	static string PkceGenerateCodeVerifier()
	{
		int len = random.Next(43, 129);  // random.Next() documented to be inclusive of lower bound, but exclusive of upper bound.
 		char[] chars = new char[len];
		for (int i = 0; i < len; i++)
		{
			chars[i] = validChars[random.Next(0, validCharsLength)];  // random.Next() documented to be inclusive of lower bound, but exclusive of upper bound.
		}
		return new string(chars);
	}
	
	private static readonly Random random = new Random();
	private const string validChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_.~";
	private static readonly int validCharsLength = validChars.Length;
	
	static string PkceGetCodeChallenge(string codeVerifier, string codeChallengeMethod)
	{
		switch(codeChallengeMethod)
		{
			case "plain":
				return PkceGetPlainCodeChallenge(codeVerifier);
			case "S256":
				return PkceGetS256CodeChallenge(codeVerifier);
			default:
				throw new ArgumentException("Must be \"plain\" or \"S256\"", nameof(codeChallengeMethod));
		}
	}
	
	static bool PkceCodeChallengeIsValid(string codeChallenge, string codeChallengeMethod)
	{
		switch(codeChallengeMethod)
		{
			case "plain":
				return PkceCodeVerifierIsValid(codeChallenge);
			case "S256":
				try
				{
					byte[] codeChallengeBytes = Base64UrlDecode(codeChallenge);
					if (codeChallengeBytes.Length != 32)
					{
						throw new ArgumentException("does not decode to byte array of length 32.", nameof(codeChallenge));
					}
					return true;
				}
				catch(Exception ex)
				{
					Console.WriteLine("Caught invalid S256 Code Challenge of " + codeChallenge);
					Console.WriteLine(ex);
					return false;
				}
			default:
				throw new ArgumentException("Must be \"plain\" or \"S256\"", nameof(codeChallengeMethod));
		}
	}
	
	static bool PkceVerify(string codeChallenge, string codeVerifier, string codeChallengeMethod)
	{
		switch(codeChallengeMethod)
		{
			case "plain":
				return codeChallenge == codeVerifier;
			case "S256":
				return Base64UrlDecode(codeChallenge).SequenceEqual(GetS256Hash(codeVerifier, Encoding.ASCII));
			default:
				throw new ArgumentException("Must be \"plain\" or \"S256\"", nameof(codeChallengeMethod));
		}
	}
	
	static bool PkceCodeVerifierIsValid(string codeVerifier)
	{
		return codeVerifier != null && codeVerifier.Length >= 43 && codeVerifier.Length <= 128 && codeVerifier.All(c => char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.' || c == '~');
	}
	
	static string ThrowIfInvalidPkceCodeVerifier(string codeVerifier)
	{
		return PkceCodeVerifierIsValid(codeVerifier) ? codeVerifier : throw new ArgumentException("Must be between 43 and 128 characters in length and entirely made up of letters and digits and periods, hyphens, underscores, and tildes.",nameof(codeVerifier));
	}
	
	static string PkceGetPlainCodeChallenge(string codeVerifier)
	{
		return ThrowIfInvalidPkceCodeVerifier(codeVerifier);
	}
	
	static string PkceGetS256CodeChallenge(string codeVerifier)
	{
		return Base64UrlEncode(GetS256Hash(ThrowIfInvalidPkceCodeVerifier(codeVerifier), Encoding.ASCII));  
	}
	
	static byte[] GetS256Hash(string str, Encoding encoding)
	{
		if (str == null)
		{
			throw new ArgumentNullException(nameof(str));
		}
		if (encoding == null)
		{
			throw new ArgumentNullException(nameof(encoding));
		}
		using (SHA256 sha256 = SHA256.Create())
		{
			return sha256.ComputeHash(encoding.GetBytes(str));
		}
	}
	
	static string Base64UrlEncode(byte[] bytes)
	{
		return Convert.ToBase64String(bytes).TrimEnd(padding).Replace('+', '-').Replace('/', '_');
	}
	
	static byte[] Base64UrlDecode(string str)
	{
		string incoming = str.Replace('_', '/').Replace('-', '+');
		switch(str.Length % 4)
		{
			case 2: incoming += "=="; break;
			case 3: incoming += "="; break;
		}
		return Convert.FromBase64String(incoming);
	}
	
	private static readonly char[] padding = { '=' };
}

...

Registering an OAuth Client App in Projector

To register your OAuth App, or to edit the registration parameters of the OAuth client app, you will need to use the Management Portal and go to the OAuth panel on the Integration Tab.  There you can create, update and delete registrations for your app (as well as manage enablement for globally available OAuth client apps, and manage user connections to the apps).  This will give you the opportunity to establish or update the name, description, redirect_url and optional logo for your client app, and to retrieve the client_id and client_secret.

...

Obtaining a New OAuth Token from Projector

The authorization endpoint in Projector includes your account code.  If (in this example) your account code is “acme-industries”, your authorization endpoint for your app would be:
              https://app.projectorpsa.com/oauth2authorize/acme-industries

...

Note that the “Authorize” portion of this flow can only be done via an HTTP GET to the authorize endpoint.  Once you get the authorization code, the documentation above shows how to exchange it for an access token using an HTTP POST request, which is part of the OAuth standard.  However, there is an alternative available if you prefer it to use the soap service PwsAcquireOauth2Token.

...

Refreshing a Projector OAuth Token

Perhaps, based on the expiry information that an access token in your possession has, your client code notices that the access token is about to expire, or perhaps a service invocation fails and the error indicates the reason is an expired or invalid session ticket.  In these cases, you will want to refresh the access token and most likely re-invoke the service with a new access token.

...

Again, if you prefer to use SOAP, you can use the PwsAcquireOauth2Token Soap service.

...

Revoking a Projector OAuth Token

If your code needs to revoke a token previously granted, thereby terminating the connection between your app and Projector, you should issue an HTTP POST request to the revocation endpoint.  If you are able to build the revocation endpoint by appending the string “/oauth2revoketoken” to the rest_service_authority on the access token you are revoking, it will be most efficient and fastest.  Following our made-up example access token above, the token refresh endpoint would be:
              https://app2.projectorpsa.com/oauth2revoketoken
but if it is difficult or inconvenient to build it that way, you can use the default token revocation endpoint of:
              https://app.projectorpsa.com/oauth2revoketoken

...

Please note that the OAuth standard anticipates the possibility of revoking just an access token (AKA Session Ticket), but leaving the associated refresh token intact.  In our server context this is something of a non-sequitur.  You can always revoke a projector session ticket with the PwsUnauthenticate Soap Service, but the restful revocation endpoint can only fully revoke refresh tokens at this time.

...

Scopes

The OAuth 2.0 Standard anticipates that client apps will request scopes  from the server when an Authentication Request is made.  The semantics of scopes is left up to the server implementation.  Projector has chosen to map scopes  to Projector Permissions, which are granted to users and applied to sessions for the session lifetime.  Please note that in the Projector OAuth Server implementation, the access tokens are session tickets, and designate a session.  The Projector Server implementation of scopes will ensure that an access token acquired through OAuth will point to a session which never has more permissions than are requested through scopes.  It may have fewer – if the authenticated user does not have the permissions being requested.

...