Skip to content

OpenSilver/OpenSilver.Auth

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 

Repository files navigation

OpenSilver Google OAuth Authentication Demo

A complete example implementing Google OAuth authentication between an OpenSilver client and ASP.NET Core backend.

a22d4ebf347d2ce81b797498bc331e94 16d140f85c1d7fdfae6c7616f8eeee26

Key Features

  • Google account login (Authorization Code Flow)
  • JWT token-based API security
  • Configuration file-based setup

Project Structure

OpenSilverAuthClient/
├── OpenSilverAuthClient.Browser/       # Client (OpenSilver)
│   ├── wwwroot/
│   │   ├── index.html                  # Google OAuth config and callback handling
│   │   └── libs/auth.js                # Authentication logic
│   └── AuthBar.xaml                    # Login UI
├── OpenSilverAuthClient.Server/        # Server (ASP.NET Core)
│   ├── Program.cs                      # Backend authentication logic
│   └── appsettings.json                # JWT and Google settings

Authentication Flow

  1. User clicks "Login with Google" button
  2. Redirects to Google login page
  3. After login completion, returns to site with authorization code
  4. Client sends code to backend API
  5. Backend exchanges code for access token with Google
  6. Backend retrieves user info and generates JWT token
  7. Client stores JWT token for authenticated API requests

Client Implementation

wwwroot/index.html

// Google OAuth configuration (change to actual values)
window.AUTH_CFG = {
    apiBase: "https://localhost:7224",  // Change to actual backend API server address
    googleClientId: "YOUR_GOOGLE_CLIENT_ID",  // Change to client ID from Google Cloud Console
    storageKey: "accessToken"
};

// Authorization code callback handling
const code = new URLSearchParams(window.location.search).get('code');
if (code) {
    fetch(window.AUTH_CFG.apiBase + "/auth/google", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ code: code })
    })
    .then(response => response.json())
    .then(data => {
        localStorage.setItem(window.AUTH_CFG.storageKey, data.accessToken);
        window.location.href = window.location.origin;  // Redirect to remove code from URL
    });
}

wwwroot/libs/auth.js

function login() {
    const params = new URLSearchParams({
        client_id: window.AUTH_CFG.googleClientId,
        redirect_uri: window.location.origin,  // Automatically uses current page origin
        response_type: 'code',
        scope: 'email profile openid',
        prompt: 'select_account'
    });
    window.location.href = `https://accounts.google.com/o/oauth2/auth?${params}`;
}

function logout() {
    localStorage.removeItem(window.AUTH_CFG.storageKey);
    window.location.reload();
}

function getToken() {
    return localStorage.getItem(window.AUTH_CFG.storageKey);
}

Server Implementation

Program.cs

Complete server-side authentication implementation:

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder(args);
var cfg = builder.Configuration;

// Load configuration values
var jwtKey = cfg["Jwt:Key"]!;
var jwtIssuer = cfg["Jwt:Issuer"]!;
var jwtAudience = cfg["Jwt:Audience"]!;
var jwtHours = cfg.GetValue("Jwt:Hours", 2);
var googleCid = cfg["Google:ClientId"]!;
var googleSecret = cfg["Google:ClientSecret"]!;
var clientOrigin = cfg["Client:Origin"]!;

// Configure CORS
builder.Services.AddCors(p => p.AddDefaultPolicy(b => 
    b.WithOrigins(clientOrigin)
     .AllowAnyHeader()
     .AllowAnyMethod()
     .AllowCredentials()));

builder.Services.AddHttpClient();

// Configure JWT Authentication
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(o => {
        o.TokenValidationParameters = new TokenValidationParameters {
            ValidateIssuer = true,
            ValidIssuer = jwtIssuer,
            ValidateAudience = true,
            ValidAudience = jwtAudience,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(1)
        };
    });

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

// Public endpoint for testing
app.MapGet("/public/ping", () => 
    Results.Ok(new { ok = true, msg = "public pong" }))
    .AllowAnonymous();

// Google OAuth to JWT token exchange endpoint
app.MapPost("/auth/google", async (GoogleExchange body, IHttpClientFactory httpClientFactory) => {
    if (string.IsNullOrWhiteSpace(body.code))
        return Results.BadRequest(new { error = "code is required" });

    var httpClient = httpClientFactory.CreateClient();

    try {
        // Step 1: Exchange authorization code for access token
        var tokenRequest = new FormUrlEncodedContent(new[] {
            new KeyValuePair<string, string>("client_id", googleCid),
            new KeyValuePair<string, string>("client_secret", googleSecret),
            new KeyValuePair<string, string>("code", body.code),
            new KeyValuePair<string, string>("grant_type", "authorization_code"),
            new KeyValuePair<string, string>("redirect_uri", clientOrigin)
        });

        var tokenResponse = await httpClient.PostAsync(
            "https://oauth2.googleapis.com/token", tokenRequest);

        if (!tokenResponse.IsSuccessStatusCode) {
            var errorContent = await tokenResponse.Content.ReadAsStringAsync();
            return Results.Problem($"Token exchange failed: {errorContent}", statusCode: 400);
        }

        var tokenJson = await tokenResponse.Content.ReadAsStringAsync();
        var tokenData = JsonSerializer.Deserialize<GoogleTokenResponse>(tokenJson);

        // Step 2: Get user information using access token
        httpClient.DefaultRequestHeaders.Authorization = 
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenData.access_token);

        var userResponse = await httpClient.GetAsync(
            "https://www.googleapis.com/oauth2/v2/userinfo");

        if (!userResponse.IsSuccessStatusCode) {
            return Results.Problem("Failed to get user info", statusCode: 400);
        }

        var userJson = await userResponse.Content.ReadAsStringAsync();
        var userData = JsonSerializer.Deserialize<GoogleUserInfo>(userJson);

        // Step 3: Create claims from user data
        var claims = new[] {
            new Claim(ClaimTypes.NameIdentifier, userData.id ?? ""),
            new Claim(ClaimTypes.Name, userData.name ?? userData.email ?? ""),
            new Claim(ClaimTypes.Email, userData.email ?? ""),
            new Claim("picture", userData.picture ?? "")
        };

        // Step 4: Generate JWT token
        var token = new JwtSecurityToken(
            issuer: jwtIssuer,
            audience: jwtAudience,
            claims: claims,
            expires: DateTime.UtcNow.AddHours(jwtHours),
            signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
        );

        return Results.Ok(new { 
            accessToken = new JwtSecurityTokenHandler().WriteToken(token) 
        });
    }
    catch (Exception ex) {
        return Results.Problem(ex.Message, statusCode: StatusCodes.Status500InternalServerError);
    }
}).AllowAnonymous();

// Protected endpoint example
app.MapGet("/secure/ping", (ClaimsPrincipal user) => {
    var name = user.Identity?.Name ?? "";
    var email = user.FindFirst(ClaimTypes.Email)?.Value ?? "";
    return Results.Ok(new { ok = true, msg = "secure pong", name, email });
}).RequireAuthorization();

app.Run();

// Record types for request/response
record GoogleExchange(string code);
record GoogleTokenResponse(string access_token, string token_type, int expires_in);
record GoogleUserInfo(string id, string email, string name, string picture);

appsettings.json

{
  "Jwt": {
    "Key": "minimum-32-character-secret-key-required-here-change-this",
    "Issuer": "OpenSilver.AuthService",
    "Audience": "OpenSilver.Clients",
    "Hours": 2
  },
  "Google": {
    "ClientId": "YOUR_GOOGLE_CLIENT_ID",
    "ClientSecret": "YOUR_GOOGLE_CLIENT_SECRET"
  },
  "Client": {
    "Origin": "http://localhost:55592"
  }
}

Pre-Execution Setup

Google Cloud Console Configuration

  1. Go to Google Cloud Console
  2. Create a new project or select existing project
  3. Enable Google+ API
  4. Go to CredentialsCreate CredentialsOAuth Client ID
  5. Select Application type: "Web application"
  6. Add your client address to Authorized redirect URIs (e.g., http://localhost:55592)
  7. Copy generated Client ID and Client Secret

Required Configuration Checklist

  • Apply Google Client ID and Secret to both index.html and appsettings.json
  • Ensure JWT Key is at least 32 characters long
  • Set correct backend API address in index.html (window.AUTH_CFG.apiBase)
  • Set correct client origin in appsettings.json (Client:Origin)
  • Verify redirect URI in Google Console matches your client address exactly

Common Issues

"redirect_uri_mismatch" error

  • Ensure the redirect URI in Google Console exactly matches your client origin
  • Check for trailing slashes or http vs https mismatches

CORS error

  • Verify Client:Origin in appsettings.json matches your actual client address
  • Ensure the server is running and accessible

JWT key length error

  • JWT Key must be at least 32 characters (256 bits)
  • Generate a secure random key for production use

Token exchange failed

  • Verify Google Client ID and Secret are correctly configured in both client and server
  • Check that the authorization code hasn't expired (they're single-use and short-lived)

Testing the Implementation

  1. Start the backend server (should run on configured port, e.g., https://localhost:7224)
  2. Start the OpenSilver client (should run on configured port, e.g., http://localhost:55592)
  3. Click "Login with Google" button
  4. Complete Google authentication
  5. Verify you're redirected back and JWT token is stored
  6. Test protected endpoints with the stored token

Security Considerations

  • Never commit secrets: Keep appsettings.json with real credentials out of source control
  • Use HTTPS in production: JWT tokens should always be transmitted over HTTPS
  • Secure token storage: Consider using httpOnly cookies instead of localStorage for better security
  • Token expiration: Implement token refresh mechanism for better user experience
  • Environment-specific configs: Use separate configurations for development and production

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published