Wolfix.Server supports Google OAuth authentication, allowing users to sign in with their Google accounts in the Identity module.
Overview
Google OAuth provides:
One-click registration - Users sign up without creating passwords
Verified emails - Google provides verified email addresses
User profile data - Get name and profile picture
Secure authentication - Leverage Google’s security infrastructure
Google Cloud Setup
Create Google Cloud Project
Enable Google+ API
Navigate to “APIs & Services” > “Library”
Search for “Google+ API”
Click “Enable”
Configure OAuth Consent Screen
Go to “APIs & Services” > “OAuth consent screen”
Select “External” user type
Fill in:
App name: “Wolfix”
User support email: your email
Developer contact: your email
Add scopes:
Add test users (for development)
Save and continue
Create OAuth 2.0 Credentials
Go to “APIs & Services” > “Credentials”
Click “Create Credentials” > “OAuth client ID”
Select “Web application”
Add authorized JavaScript origins:
http://localhost:3000 (development)
https://yourdomain.com (production)
Add authorized redirect URIs:
http://localhost:3000/auth/callback (development)
https://yourdomain.com/auth/callback (production)
Click “Create”
Copy Client ID
Configuration
Environment Variables
Add to .env file:
# Google OAuth
GOOGLE_PASSWORD = your-secure-random-password
GOOGLE_PASSWORD is used internally to create accounts for Google users. It should be a secure random string that users never see.
Module Registration
Google OAuth is configured in the Identity module:
Wolfix.API/Extensions/WebApplicationBuilderExtension.cs
private static WebApplicationBuilder AddIdentityModule (
this WebApplicationBuilder builder ,
string connectionString )
{
string tokenIssuer = builder . Configuration . GetOrThrow ( "TOKEN_ISSUER" );
string tokenAudience = builder . Configuration . GetOrThrow ( "TOKEN_AUDIENCE" );
string tokenKey = builder . Configuration . GetOrThrow ( "TOKEN_KEY" );
string tokenLifetime = builder . Configuration . GetOrThrow ( "TOKEN_LIFETIME" );
builder . Services . AddIdentityModule (
connectionString ,
tokenIssuer ,
tokenAudience ,
tokenKey ,
tokenLifetime
);
return builder ;
}
Implementation
Google Login DTO
Identity.Application/Dto/Requests/GoogleLoginDto.cs
namespace Identity . Application . Dto . Requests ;
public sealed record GoogleLoginDto ( string IdToken );
Authentication Service
The AuthService handles Google authentication:
Identity.Application/Services/AuthService.cs
using Google . Apis . Auth ;
using GooglePayload = Google . Apis . Auth . GoogleJsonWebSignature . Payload ;
public sealed class AuthService
{
private readonly IAuthStore _authStore ;
private readonly JwtService _jwtService ;
private readonly EventBus _eventBus ;
private readonly string _googlePassword ;
public AuthService (
IAuthStore authStore ,
JwtService jwtService ,
IConfiguration configuration ,
EventBus eventBus )
{
_authStore = authStore ;
_jwtService = jwtService ;
_eventBus = eventBus ;
_googlePassword = configuration [ "GOOGLE_PASSWORD" ]
?? throw new Exception ( "GOOGLE_PASSWORD is not set" );
}
public async Task < Result < UserRolesDto >> ContinueWithGoogleAndGetRolesAsync (
GooglePayload payload ,
CancellationToken ct )
{
// Check if user exists
Result < Guid > checkUserExistsResult = await _authStore . CheckUserExistsAsync (
payload . Email ,
ct
);
UserRolesDto dto ;
if ( checkUserExistsResult . IsFailure )
{
// New user - register via Google
Result < Guid > registerViaGoogleResult = await RegisterViaGoogle (
payload ,
_googlePassword ,
ct
);
if ( registerViaGoogleResult . IsFailure )
{
return Result < UserRolesDto >. Failure ( registerViaGoogleResult );
}
Guid accountId = registerViaGoogleResult . Value ;
dto = new UserRolesDto (
accountId ,
payload . Email ,
[ Roles . Customer ]
);
}
else
{
// Existing user - login via Google
Result < UserRolesProjection > getUserRolesResult =
await _authStore . LogInViaGoogleAndGetUserRolesAsync (
payload . Email ,
_googlePassword ,
ct
);
if ( getUserRolesResult . IsFailure )
{
return Result < UserRolesDto >. Failure ( getUserRolesResult );
}
dto = getUserRolesResult . Value ! . ToDto ();
}
return Result < UserRolesDto >. Success ( dto );
}
private async Task < Result < Guid >> RegisterViaGoogle (
GooglePayload payload ,
string password ,
CancellationToken ct )
{
// 1. Create account
Result < Guid > registerAccountResult = await _authStore . RegisterAccountAsync (
payload . Email ,
password ,
Roles . Customer ,
ct ,
"Google"
);
if ( registerAccountResult . IsFailure )
{
return Result < Guid >. Failure ( registerAccountResult );
}
Guid accountId = registerAccountResult . Value ;
// 2. Publish event to create customer profile
var @event = new CustomerAccountCreatedViaGoogle
{
AccountId = accountId ,
LastName = payload . FamilyName ,
FirstName = payload . GivenName ,
PhotoUrl = payload . Picture
};
Result < Guid > createCustomerResult = await _eventBus
. PublishWithSingleResultAsync < CustomerAccountCreatedViaGoogle , Guid >(
@event ,
ct
);
if ( createCustomerResult . IsFailure )
{
return Result < Guid >. Failure ( createCustomerResult );
}
return Result < Guid >. Success ( createCustomerResult . Value );
}
}
API Endpoint
Identity.Endpoints/Endpoints/AuthEndpoints.cs
public static IEndpointRouteBuilder MapAuthEndpoints ( this IEndpointRouteBuilder app )
{
var group = app . MapGroup ( "/api/auth" )
. WithTags ( "Authentication" );
group . MapPost ( "/google" , GoogleLogin )
. AllowAnonymous ();
return app ;
}
private static async Task < IResult > GoogleLogin (
[ FromBody ] GoogleLoginDto dto ,
[ FromServices ] AuthService authService ,
[ FromServices ] IConfiguration configuration ,
CancellationToken ct )
{
try
{
// 1. Verify Google ID token
string clientId = configuration [ "GOOGLE_CLIENT_ID" ] ! ;
GoogleJsonWebSignature . Payload payload =
await GoogleJsonWebSignature . ValidateAsync ( dto . IdToken );
// 2. Verify audience matches our client ID
if ( payload . Audience != clientId )
{
return Results . Unauthorized ();
}
// 3. Get or create user
Result < UserRolesDto > result =
await authService . ContinueWithGoogleAndGetRolesAsync ( payload , ct );
if ( result . IsFailure )
{
return Results . Problem (
detail : result . ErrorMessage ,
statusCode : ( int ) result . StatusCode
);
}
return Results . Ok ( result . Value );
}
catch ( InvalidJwtException )
{
return Results . Unauthorized ();
}
}
Frontend Integration
Install Google Identity Services
npm install @react-oauth/google
import { GoogleOAuthProvider } from '@react-oauth/google' ;
function App () {
return (
< GoogleOAuthProvider clientId = "YOUR_GOOGLE_CLIENT_ID" >
< YourApp />
</ GoogleOAuthProvider >
);
}
import { GoogleLogin } from '@react-oauth/google' ;
import { useState } from 'react' ;
function LoginPage () {
const [ error , setError ] = useState ( null );
const handleGoogleSuccess = async ( credentialResponse ) => {
try {
// Send ID token to backend
const response = await fetch ( 'http://localhost:5000/api/auth/google' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
idToken: credentialResponse . credential
})
});
if ( ! response . ok ) {
throw new Error ( 'Login failed' );
}
const data = await response . json ();
// Data contains accountId, email, and available roles
console . log ( 'User roles:' , data . roles );
// Navigate to role selection or dashboard
if ( data . roles . length === 1 ) {
// Only one role - get token directly
await getTokenForRole ( data . email , data . roles [ 0 ]);
} else {
// Multiple roles - show selection
showRoleSelection ( data );
}
} catch ( err ) {
setError ( err . message );
}
};
const handleGoogleError = () => {
setError ( 'Google login failed' );
};
const getTokenForRole = async ( email , role ) => {
const response = await fetch ( 'http://localhost:5000/api/auth/token' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
email: email ,
password: '' , // Not needed for Google users
role: role
})
});
const { token } = await response . json ();
// Store token
localStorage . setItem ( 'jwt' , token );
// Navigate to dashboard
window . location . href = '/dashboard' ;
};
return (
< div >
< h1 > Login </ h1 >
< GoogleLogin
onSuccess = { handleGoogleSuccess }
onError = { handleGoogleError }
useOneTap
/>
{ error && < p className = "error" > { error } </ p > }
</ div >
);
}
Authentication Flow
User Clicks Google Button
Frontend shows Google login popup.
User Authorizes
User logs in with Google and authorizes the app.
Frontend Receives ID Token
Google returns an ID token (JWT) to the frontend.
Frontend Sends Token to Backend
POST / api / auth / google
{
"idToken" : "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Backend Verifies Token
GoogleJsonWebSignature . Payload payload =
await GoogleJsonWebSignature . ValidateAsync ( idToken );
Backend Creates/Finds User
New user: Create account + customer profile
Existing user: Login and return roles
Frontend Gets JWT Token
User selects role and gets JWT token: POST / api / auth / token
{
"email" : "[email protected] " ,
"password" : "" ,
"role" : "Customer"
}
User Authenticated
Frontend stores JWT and user is logged in.
Integration Event
Event Contract
Identity.IntegrationEvents/CustomerAccountCreatedViaGoogle.cs
public class CustomerAccountCreatedViaGoogle
{
public Guid AccountId { get ; init ; }
public string FirstName { get ; init ; } = string . Empty ;
public string LastName { get ; init ; } = string . Empty ;
public string PhotoUrl { get ; init ; } = string . Empty ;
}
Event Handler (Customer Module)
Customer.Application/EventHandlers/CustomerAccountCreatedViaGoogleHandler.cs
public class CustomerAccountCreatedViaGoogleHandler
: IIntegrationEventHandler < CustomerAccountCreatedViaGoogle , Guid >
{
private readonly ICustomerRepository _customerRepository ;
public async Task < Result < Guid >> HandleAsync (
CustomerAccountCreatedViaGoogle @event ,
CancellationToken ct )
{
// Create customer with Google profile data
Result < Customer > createResult = Customer . Create (
@event . AccountId ,
@event . FirstName ,
@event . LastName
);
if ( createResult . IsFailure )
return Result < Guid >. Failure ( createResult );
Customer customer = createResult . Value ! ;
// Set profile photo URL if provided
if ( ! string . IsNullOrEmpty ( @event . PhotoUrl ))
{
customer . SetProfilePhotoUrl ( @event . PhotoUrl );
}
await _customerRepository . AddAsync ( customer , ct );
await _customerRepository . SaveChangesAsync ( ct );
return Result < Guid >. Success ( customer . Id );
}
}
Security Considerations
Always Verify ID Token
Never trust the ID token without verification: GoogleJsonWebSignature . Payload payload =
await GoogleJsonWebSignature . ValidateAsync ( idToken );
Verify Audience
Ensure the token was issued for your app: if ( payload . Audience != clientId )
{
return Results . Unauthorized ();
}
Check Token Expiration
Google library handles this automatically, but you can check: if ( payload . ExpirationTimeSeconds < DateTimeOffset . UtcNow . ToUnixTimeSeconds ())
{
return Results . Unauthorized ();
}
Use HTTPS in Production
Google OAuth requires HTTPS for redirect URIs in production.
Testing
Test Locally
Configure authorized origins: http://localhost:3000
Add test users in Google Cloud Console
Use test user credentials to sign in
Mock Google Authentication (Unit Tests)
[ Fact ]
public async Task GoogleLogin_WithValidToken_ShouldCreateUser ()
{
// Arrange
var mockAuthStore = new Mock < IAuthStore >();
var mockEventBus = new Mock < EventBus >();
mockAuthStore . Setup ( x => x . CheckUserExistsAsync ( It . IsAny < string >(), It . IsAny < CancellationToken >()))
. ReturnsAsync ( Result < Guid >. Failure ( "Not found" ));
mockAuthStore . Setup ( x => x . RegisterAccountAsync (
It . IsAny < string >(),
It . IsAny < string >(),
It . IsAny < string >(),
It . IsAny < CancellationToken >(),
"Google"
))
. ReturnsAsync ( Result < Guid >. Success ( Guid . NewGuid ()));
var service = new AuthService ( mockAuthStore . Object , null ! , null ! , mockEventBus . Object );
var payload = new GoogleJsonWebSignature . Payload
{
Email = "[email protected] " ,
GivenName = "Test" ,
FamilyName = "User" ,
Picture = "https://..."
};
// Act
var result = await service . ContinueWithGoogleAndGetRolesAsync ( payload , CancellationToken . None );
// Assert
Assert . True ( result . IsSuccess );
Assert . Equal ( "[email protected] " , result . Value ! . Email );
}
Troubleshooting
Invalid ID Token
Issue: “Invalid JWT signature”
Solution: Ensure client ID matches the one used in frontend.
Unauthorized Redirect URI
Issue: “redirect_uri_mismatch”
Solution: Add redirect URI to Google Cloud Console authorized URIs.
User Already Exists
Issue: User registered with email/password, then tries Google login.
Solution: Link Google account to existing account:
public async Task < VoidResult > LinkGoogleAccountAsync ( Guid accountId , string googleId )
{
// Store Google ID with account
// Allow login with either method
}
Next Steps
Toxicity API Content moderation integration
Stripe Payments Payment processing