Thursday, August 4, 2022

Implementing OpenID auth with Ping

  • On JavaScript/TypeScript platforms, make sure to pick a generic OIDC client library such as @openid/AppAuth-JS or angular-auth-oidc-client. If you use some of the Ping-published libraries being used in their examples - such as @ping-identity/p14c-js-sdk-auth - that depend on features specific to Ping's hosted environments, you would face issues when deploying against other environments like PingFederate.
  • Ping hosted environments include the signing (JWT) public key - embedded in a X.509 certificate - in their JWKS endpoint responses (under a x5j field); but relying on that field would make your solution incompatible with other environments/providers, which may not contain this readily-usable field. Instead you can derive the key using the standard modulus n and exponent e from the JWKS JSON:
    import java.security.spec.RSAPublicKeySpec;
    import org.springframework.util.Base64Utils;
    
    // key is a map containing JWKS spec
    
    RSAPublicKeySpec spec = new RSAPublicKeySpec(
    	new BigInteger(1, Base64Utils.decodeFromUrlSafeString((String) key.get("n"))),
    	new BigInteger(1, Base64Utils.decodeFromUrlSafeString(((String) key.get("e")))));
    return keyFactory.generatePublic(spec);
  • If you are developing specifically for the Ping hosted environment, using @pingidentity/p14c-js-sdk-auth, note these regarding single-page app (SPA) mode:
    • The library has non-standard parameters (e.g. environment_id, and AUTH_URL/APP_URL for environments hosted in non-.com domains/regions) and conditions (e.g. environment and client IDs must be in UUID format). However other standard OIDC libraries can be used against the same environments without these additional parameters/constraints.
    • In implicit (token) grant, you can extract the token from the callback URL using client.parseRedirectUrl() followed by client.tokenManager.get('idToken'), which returns a Promise containing parsed URL data (having an idToken or similar field).
    • When using PKCE under authorization code grant, you can extract the code_verifier used by the Ping client using client.tokenManager.pkce.loadData().codeVerifier (for the code-to-token exchange phase).

A typical implementation that supports both implicit and auth. code grants, in Angular, may look like:

import {PingOneAuthClient} from "@ping-identity/p14c-js-sdk-auth/dist/@ping-identity/p14c-js-sdk-auth";

type PingClient = {
	signIn();
	signOut();
	revokeToken(token, tokenName);
	parseRedirectUrl (options?);
	getUserInfo();
	tokenManager: {
		get(tokenName: string): {
			value: string
		};
		pkce: {
			loadData(): {
			codeVerifier: string
			}
		};
	};
	config: {
		responseType: Array<string>
	};
};

// from a typical Ping hosted environment
const pingAuth = {
	"AUTH_URI": "https://auth.pingone.asia",
	"APP_URI": "https://app.pingone.asia",
	"environmentId": "your-env-id-uuid",
	"clientId": "your-client-id-uuid",
	"scopes": ["openid"],
	"responseType": ["id_token"],
	"storage": "localStorage",
	"pkce": false	// make sure to revise, based on your security/compliance needs
};

// .. component code; need Http, ActivatedRoute

	async getClient(): Promise<PingClient> {
		return new PingOneAuthClient({
			...pingAuth,
			redirectUri: `${window.location.origin}/openid-callback`	// your callback URL
		});
	}

	async initiateAuth() {
		return (await this.getClient()).signIn();
	}

	async completeAuth() {
		const client = await this.getClient();

		if (client.config.responseType.includes('id_token')) {	// implicit
			return client.parseRedirectUrl()
				.then(() => client.tokenManager.get('idToken'))
				.then(token => token.idToken);

		} else {
			const code = this.route.snapshot.queryParams['code'];	// auth.code

			let contentHeader: Headers = new Headers();
			contentHeader.append("Content-Type", "application/json");
			return this.http.post(/* your code-exchange backend URL */, JSON.stringify({
				code,
				verifier: client.tokenManager.pkce.loadData().codeVerifier
			}), {
				headers: contentHeader
			}).toPromise()
				.then(response => response.json()['id_token'])	// depends on your backend
				.catch(e => /* handle it */);
	}

No comments: