| Begriff | Bedeutung |
|---|---|
| Authorization Server | stellt Tokens aus |
| Resource Server | schützt API mit Token |
| Client | App, die Token anfordert |
| Access Token | Zugriffsnachweis (kurzlebig) |
| Authorization Code | einmaliger Einlösecode |
Client Browser Auth-Server API
│──── Auth-URL ────▶│ │ │
│ │─── GET /authorize ──▶│ │
│ │◀── Login-Formular ───│ │
│ │──── POST Login ──────▶│ │
│◀── code (Redirect)──────────────────────│ │
│──── POST /token (code + verifier) ──────▶│ │
│◀── access_token ────────────────────────│ │
│──────────────────── GET /api/hello ─────────────▶│
│◀─────────────────── { message } ────────────────│
code_verifier (32 Bytes, Base64Url)code_challenge = SHA256(verifier), Base64Url-kodiertOpenIddict.AspNetCore, OpenIddict.EntityFrameworkCoreAuthentication.Core/ ← PkceHelper (PKCE-Logik)
Authentication.Server/ ← OpenIddict OIDC-Server (Port 5001)
Authentication.Client/ ← Console-Client (PKCE-Flow)
Authentication.Tests/ ← xUnit (RFC 7636 Testvektor)
public static class PkceHelper
{
public static string CreateVerifier()
{
var bytes = RandomNumberGenerator.GetBytes(32);
return Base64UrlEncode(bytes);
}
public static string CreateChallenge(string verifier)
{
var bytes = SHA256.HashData(Encoding.ASCII.GetBytes(verifier));
return Base64UrlEncode(bytes);
}
}builder.Services.AddOpenIddict()
.AddCore(o => o.UseEntityFrameworkCore()
.UseDbContext<AppDbContext>())
.AddServer(o =>
{
o.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token");
o.AllowAuthorizationCodeFlow()
.RequireProofKeyForCodeExchange();
o.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
o.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.DisableTransportSecurityRequirement();
});await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = "demo-app",
ClientType = ClientTypes.Public, // kein Client Secret
RedirectUris = { new Uri("http://localhost:5002/callback") },
Permissions =
{
Permissions.Endpoints.Authorization,
Permissions.Endpoints.Token,
Permissions.GrantTypes.AuthorizationCode,
Permissions.ResponseTypes.Code,
Permissions.Scopes.Profile,
}
});app.MapGet("/connect/authorize", async (HttpContext ctx) =>
{
var result = await ctx.AuthenticateAsync(
CookieAuthenticationDefaults.AuthenticationScheme);
if (!result.Succeeded)
return Results.Redirect($"/account/login?returnUrl=…");
var request = ctx.GetOpenIddictServerRequest()!;
var identity = new ClaimsIdentity(
[new Claim(Claims.Subject, result.Principal!.Identity!.Name!),
new Claim(Claims.Name, result.Principal!.Identity!.Name!)],
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.SetScopes(request.GetScopes());
return Results.SignIn(new ClaimsPrincipal(identity),
authenticationScheme:
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
});var verifier = PkceHelper.CreateVerifier();
var challenge = PkceHelper.CreateChallenge(verifier);
var state = Base64UrlRandom();
// Browser öffnen
Process.Start(new ProcessStartInfo(authUrl) { UseShellExecute = true });
// Callback abhören
using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:5002/");
listener.Start();
var ctx = await listener.GetContextAsync();
var code = HttpUtility.ParseQueryString(ctx.Request.Url!.Query)["code"]!;
// Token tauschen
var tokenResponse = await http.PostAsync("/connect/token",
new FormUrlEncodedContent(new Dictionary<string, string> {
["grant_type"] = "authorization_code", ["code"] = code,
["redirect_uri"] = RedirectUri, ["client_id"] = ClientId,
["code_verifier"] = verifier }));dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkE9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cMOpenIddict.AspNetCore 7.4.0, OpenIddict.EntityFrameworkCore 7.4.0Authentication.Core ← Domain
GitHubUser (record)
IGitHubAuthService Port
LoginViewModel (MVVM)
Authentication.Infrastructure ← Adapter
GitHubAuthService OAuth + OidcClient
Authentication ← MAUI-App
MauiBrowser WebAuthenticator
MauiProgram DI-Setup
MainPage.xaml UI
@startuml
actor User
participant "MAUI App" as App
participant "System Browser" as Browser
participant "GitHub" as GitHub
User -> App: Login-Button
App -> Browser: WebAuthenticator.AuthenticateAsync()\n(Authorization URL + PKCE)
Browser -> GitHub: GET /login/oauth/authorize
GitHub -> Browser: Login-Seite
User -> Browser: Anmeldung bestätigen
Browser -> App: mauigithubauth://callback?code=…
App -> GitHub: POST /login/oauth/access_token
GitHub -> App: access_token
App -> GitHub: GET /user (Bearer Token)
GitHub -> App: GitHubUser (Login, Name, Avatar)
App -> User: Profil anzeigen
@endumlCFBundleURLTypes in Info.plist<intent-filter> in AndroidManifest.xmlmauigithubauth://callbackpublic class MauiBrowser : IBrowser
{
public async Task<BrowserResult> InvokeAsync(
BrowserOptions options, CancellationToken ct)
{
var result = await WebAuthenticator.Default
.AuthenticateAsync(
new Uri(options.StartUrl),
new Uri(options.EndUrl));
var url = options.EndUrl
+ "?" + string.Join("&",
result.Properties
.Select(kvp => $"{kvp.Key}={kvp.Value}"));
return new BrowserResult
{
Response = url,
ResultType = BrowserResultType.Success
};
}
}var options = new OidcClientOptions
{
ClientId = "YOUR_CLIENT_ID",
ClientSecret = "YOUR_CLIENT_SECRET",
Scope = "read:user",
RedirectUri = "mauigithubauth://callback",
Browser = _browser,
ProviderInformation = new ProviderInformation
{
IssuerName = "https://github.com",
AuthorizeEndpoint =
"https://github.com/login/oauth/authorize",
TokenEndpoint =
"https://github.com/login/oauth/access_token",
}
};
var client = new OidcClient(options);
var loginResult = await client.LoginAsync();var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
http.DefaultRequestHeaders.UserAgent
.ParseAdd("csharp-maui-github-auth");
var response = await http.GetStringAsync(
"https://api.github.com/user");
var dto = JsonSerializer.Deserialize<GitHubUserDto>(response);
return new GitHubUser(dto.Login, dto.Name, dto.AvatarUrl);IBrowser → MAUI-App-Schicht (WebAuthenticator)IGitHubAuthService → Infrastructure (OidcClient)LoginViewModel → Core (keine MAUI-Abhängigkeit)[Fact]
public async Task LoginAsync_SetsUserProperties_OnSuccess()
{
var service = new FakeGitHubAuthService("token",
new GitHubUser("robinnunkesser", "Robin", "…"));
var vm = new LoginViewModel(service);
await vm.LoginCommand.ExecuteAsync(null);
Assert.True(vm.IsLoggedIn);
Assert.Equal("robinnunkesser", vm.Login);
}dotnet test ohne MAUI-Toolchainhttps://github.com/RobinNunkessermauigithubauth://callbackGitHubAuthService.cs eintragenNuGet: IdentityModel.OidcClient 6.0.0, CommunityToolkit.Mvvm 8.4.2
Kein Account, kein externer Dienst, vollständig offline lauffähig