...
Excerpt | ||
---|---|---|
| ||
Guide for Projector Citizen Integrators |
...
Some Implementation Notes
A Note about Access Tokens
...
Code Block | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
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.
...