GAP School Module 05 — Customer Portal Lesson 5.2

Password-based registration for a customer portal is a friction source and a liability. Customers forget passwords, use weak ones, and abandon registration flows that ask them to create yet another account. OAuth — letting customers log in with an account they already have — removes the friction and the password storage problem simultaneously. Three providers cover 95%+ of the target customer base: Google, Apple, and Microsoft.


The situation

The portal needed login without creating a new password management problem. The customers are not technical — they’re buyers and consignors. A registration flow that asks for email + password + confirmation + email verification would have sub-20% completion. The alternative: one click with an account they already trust.


What I did

The OAuth flow

All three providers use the same basic OAuth 2.0 flow:

  1. User clicks “Sign in with Google/Apple/Microsoft”
  2. Browser redirects to the provider’s authorization endpoint
  3. User authenticates with the provider
  4. Provider redirects back to the site with an authorization code
  5. Site exchanges the code for an access token + user info
  6. Site creates or updates the WordPress user record and logs them in

WordPress creates a user with wp_insert_user() on first login and calls wp_set_auth_cookie() to establish the session. Subsequent logins find the existing user by the stored provider user ID.

Google OAuth

Google login URL — state parameter CSRF protection via transient
function [client]_google_login_url(): string { $state = wp_create_nonce( '[client]_google_oauth' ); set_transient( 'google_oauth_state_' . $state, 1, 300 ); // 5-min window $params = http_build_query( [ 'client_id' => [client]_get_setting( 'google_client_id' ), 'redirect_uri' => [client]_oauth_callback_url( 'google' ), 'response_type' => 'code', 'scope' => 'openid email profile', 'state' => $state, ] ); return 'https://accounts.google.com/o/oauth2/v2/auth?' . $params; } function [client]_handle_google_callback( string $code, string $state ): int|false { // Verify state token if ( ! get_transient( 'google_oauth_state_' . $state ) ) { return false; } delete_transient( 'google_oauth_state_' . $state ); // Exchange code for token $token_response = wp_remote_post( 'https://oauth2.googleapis.com/token', [ 'body' => [ 'code' => $code, 'client_id' => [client]_get_setting( 'google_client_id' ), 'client_secret' => [client]_get_setting( 'google_client_secret' ), 'redirect_uri' => [client]_oauth_callback_url( 'google' ), 'grant_type' => 'authorization_code', ], ] ); if ( is_wp_error( $token_response ) ) return false; $tokens = json_decode( wp_remote_retrieve_body( $token_response ), true ); $id_token = $tokens['id_token'] ?? ''; // Decode JWT payload (middle segment, base64) $payload = json_decode( base64_decode( explode( '.', $id_token )[1] ), true ); return [client]_find_or_create_portal_user( 'google', $payload['sub'], sanitize_email( $payload['email'] ), sanitize_text_field( $payload['name'] ?? '' ) ); }

Apple OAuth — the quirks

Apple has two notable differences from Google and Microsoft. First, Apple only sends the user’s name on the first authorization — subsequent logins send the user ID and email only. Store the name on first login; you won’t see it again. Second, Apple’s JWT verification requires validating the signature against Apple’s public key.

Apple callback — first-login-only name handling
function [client]_handle_apple_callback( string $code, string $state, array $user_data = [] ): int|false { // $user_data is only populated on FIRST login — Apple omits it on subsequent logins $first_login = ! empty( $user_data ); // ... token exchange omitted for brevity ... // Store name ONLY if present (first login) $display_name = ''; if ( $first_login && ! empty( $user_data['name'] ) ) { $display_name = trim( $user_data['name']['firstName'] . ' ' . $user_data['name']['lastName'] ); } return [client]_find_or_create_portal_user( 'apple', $apple_user_id, $email, $display_name ); }

Microsoft OAuth

Microsoft uses the same OAuth 2.0 pattern but requires registering an app in Azure Active Directory. The token endpoint is tenant-specific. Use common as the tenant to accept both personal Microsoft accounts and organizational accounts: https://login.microsoftonline.com/common/oauth2/v2.0/token.

find_or_create_portal_user

User lookup — provider ID first, email fallback, then create with restricted role
function [client]_find_or_create_portal_user( string $provider, string $provider_user_id, string $email, string $display_name = '' ): int|false { // Look for existing user by provider ID stored in user meta $existing_users = get_users( [ 'meta_key' => '[client]_oauth_' . $provider . '_id', 'meta_value' => $provider_user_id, 'number' => 1, ] ); if ( ! empty( $existing_users ) ) { return $existing_users[0]->ID; } // Check if email already exists (different provider, same person) $by_email = get_user_by( 'email', $email ); if ( $by_email ) { update_user_meta( $by_email->ID, '[client]_oauth_' . $provider . '_id', $provider_user_id ); return $by_email->ID; } // Create new user with restricted portal role $user_id = wp_insert_user( [ 'user_login' => $email, 'user_email' => $email, 'display_name' => $display_name ?: $email, 'role' => '[client]_portal_customer', 'user_pass' => wp_generate_password( 32 ), ] ); if ( is_wp_error( $user_id ) ) return false; update_user_meta( $user_id, '[client]_oauth_' . $provider . '_id', $provider_user_id ); // Link to existing CRM lead if email matches $lead = [client]_get_lead_by_email( $email ); if ( $lead ) { update_user_meta( $user_id, '[client]_portal_lead_id', $lead->id ); } return $user_id; }

The [client]_portal_customer role is a custom role with no admin capabilities. Portal users can only access portal pages — nothing in WordPress admin.


Why it matters

Password-based registration for a small-business customer portal produces low completion rates and ongoing password-reset support requests. OAuth offloads the credential management problem to Google/Apple/Microsoft, who are better at it, and produces a one-click login that customers actually complete.

The email-matching fallback in find_or_create_portal_user() handles the case where a customer used Google last time and tries Apple this time. Same email address → same user record, even across different providers.


The Anchor build

All three providers were implemented. Google accounted for 68% of portal logins, Microsoft 19%, Apple 13%. The Apple implementation required the most care — the name-only-on-first-login behavior caused a bug in the first version where returning Apple users had blank display names. Fixed by storing the name on first login and not overwriting it on subsequent ones.


Do this, not that

  • Use a state parameter and verify it on callback. Without state verification, the callback endpoint is vulnerable to CSRF attacks. The transient pattern (create → verify → delete) is the correct implementation.
  • Store the provider user ID in user meta, not just the email. The same email address can authenticate via different providers. The provider ID is the stable, unique identifier.
  • Handle Apple’s first-login-only name immediately. Store it the moment it arrives. You cannot retrieve it again from Apple’s API later.
  • Create a custom restricted role for portal customers. A named custom role makes permission audits unambiguous and prevents future role-permission changes from affecting portal users unexpectedly.
  • Link portal users to their CRM lead record at creation. The portal’s value comes from showing the customer their data. That data lives in the CRM. The link makes it accessible without runtime email lookups on every portal page load.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.

None of this gates a single lesson. The curriculum was free before you got here and it stays that way.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.

See GAP Concierge →