I've created and OAuth authorization server using ASP.NET Core(.net 7).This is authentication scheme in the authorization server:
builder.Services.AddAuthentication("Cookie")
.AddCookie("Cookie", options =>
{
options.LoginPath = $"/login";
});
And this are the login, authorize and token endpoints:
app.MapGet("/login", async (HttpContext context,string ReturnUrl) =>
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier,Guid.NewGuid().ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, "Cookie");
var user = new ClaimsPrincipal(claimsIdentity);
await context.SignInAsync("Cookie", user);
return Results.Redirect(ReturnUrl);
});
app.MapGet("/oauth/authorize", (HttpRequest request,IDataProtectionProvider dataProtectionProvider) =>
{
request.Query.TryGetValue("response_type", out var responseType);
request.Query.TryGetValue("client_id", out var clientId);
request.Query.TryGetValue("code_challenge", out var codeChallenge);
request.Query.TryGetValue("code_challenge_method", out var codeChallengeMethod);
request.Query.TryGetValue("redirect_uri", out var redirectUri);
request.Query.TryGetValue("scope", out var scope);
request.Query.TryGetValue("state", out var state);
var issuer = request.Scheme + "://" + request.Host.Value;
var protector = dataProtectionProvider.CreateProtector("oauth");
var code = new OAuthCode
(
clientId!,
codeChallenge!,
codeChallengeMethod!,
redirectUri!,
DateTime.UtcNow.AddMinutes(5)
);
var codeJson = JsonSerializer.Serialize(code);
var codeString = protector.Protect(codeJson);
return Results.Redirect($"{redirectUri}?code={codeString}&state={state}&iss={issuer}");
})
.RequireAuthorization("Authorized");
app.MapPost("/oauth/token", async (HttpRequest request, DevKeys devKeys,IDataProtectionProvider protectionProvider) =>
{
var bodyBytes = await request.BodyReader.ReadAsync();
var bodyContent = Encoding.UTF8.GetString(bodyBytes.Buffer);
string grantType = "", code = "", redirectUri = "", codeVerifier = "";
var regex = new Regex
(@"client_id=(?<client_id>.+?)&redirect_uri=(?<redirect_uri>.+?)&client_secret=(?<client_secret>.+?)&code=(?<code>.+?)&grant_type=(?<grant_type>.+?)&code_verifier=(?<code_verifier>.+?)");
var match = regex.Match(bodyContent);
if (match.Success)
{
grantType = match.Groups["grant_type"].Value;
code = match.Groups["code"].Value;
redirectUri = match.Groups["redirect_uri"].Value;
}
else
return Results.BadRequest("Token endpoint parameters are not speicifed");
codeVerifier = bodyContent.Split('&').Last().Split('=').Last();
var protector = protectionProvider.CreateProtector("oauth");
var codeString = protector.Unprotect(code);
OAuthCode? oAuthCode = JsonSerializer.Deserialize<OAuthCode>(codeString);
if(oAuthCode == null)
return Results.BadRequest("Invalid Code");
using(var sha256 = SHA256.Create())
{
//Get the code challenge from the code verifier
var codeChallenge = WebEncoders.Base64UrlEncode(sha256.ComputeHash(Encoding.ASCII.GetBytes(codeVerifier)));
if (oAuthCode.CodeChallenge != codeChallenge)
return Results.BadRequest("Code Challenge Mismatch");
}
var handler = new JsonWebTokenHandler();
var result = new
{
access_token = handler.CreateToken(new SecurityTokenDescriptor
{
Claims = new Dictionary<string, object>
{
[JwtRegisteredClaimNames.Sub] = Guid.NewGuid().ToString(),
["SomeCustomClaim"] = "CustomClaimValue"
},
Expires = DateTime.UtcNow.AddMinutes(100),
TokenType = "Bearer",
SigningCredentials = new SigningCredentials(devKeys.RsaSecurityKey, SecurityAlgorithms.RsaSha256)
}),
token_type = "Bearer"
};
return Results.Ok(result);
});
I have also create a client application using ASP.NET Core(.net 7) and set up Main Auth Scheme and OAuth Scheme in the following way:
builder.Services.AddAuthentication("Cookie")
.AddCookie("Cookie", options =>
{
var del = options.Events.OnRedirectToAccessDenied;
options.Events.OnRedirectToAccessDenied = async context =>
{
if (context.Request.Path.StartsWithSegments("/CustomServer"))
await context.HttpContext.ChallengeAsync("CustomServer", new AuthenticationProperties { RedirectUri = context.Request.Path });
else
context.Response.Redirect("/login");
};
})
.AddOAuth("CustomServer", options =>
{
options.ClientId = "NotRealClientId";
options.ClientSecret = "NotRealClientIdSecret";
options.AuthorizationEndpoint = "https://localhost:7048/oauth/authorize";
options.TokenEndpoint = "https://localhost:7048/oauth/token";
options.CallbackPath = "/oauth/CustomServer-cb";
options.UserInformationEndpoint = "https://localhost:7048/user";
options.SignInScheme = "Cookie";
options.SaveTokens = true;
options.UsePkce = true;
options.ClaimActions.MapJsonKey("sub", "sub");
options.ClaimActions.MapJsonKey("SomeCustomClaim", "SomeCustomClaim");
options.Events.OnCreatingTicket = async ctx =>
{
var oAuthHandlerProvider = ctx.HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
var handler = await oAuthHandlerProvider.GetHandlerAsync(ctx.HttpContext, "Cookie");
var result = await handler!.AuthenticateAsync();
if (!result.Succeeded)
{
ctx.Fail("Faild Authentication");
return;
}
var user = ctx.HttpContext.User;
var payLoadBase64 = ctx.AccessToken!.Split('.')[1];
var payLoadJson = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(payLoadBase64));
var payLoad = JsonDocument.Parse(payLoadJson);
ctx.Principal = user.Clone();
ctx.Principal?.Identities.First(x => x.AuthenticationType == "Cookie")
.AddClaim(new Claim("CustomServer-authorized", "true"));
ctx.RunClaimActions(payLoad.RootElement);
};
})
The Problem is the following:
In my client APP in OnCreatingTicket Event, the result of the following line is always Failure:
var result = await handler!.AuthenticateAsync();
So I can't resolve the user session and can't get the userId to associate the new AccessToken in the database.
Failure message: {"Unprotect ticket failed"}. I need to mention that **Access Token is successfully **retrieved.
On the other hand I also try to implement the client side of the OAuth for the GitHub API in this way:
.AddOAuth("Github", options =>
{
options.ClientId = "NotRealClientId";
options.ClientSecret = "NotRealClientSecret";
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.CallbackPath = "/oauth/github-cb";
options.UserInformationEndpoint = "https://api.github.com/user";
options.SignInScheme = "Cookie";
options.SaveTokens = true;
options.Events.OnCreatingTicket = async ctx =>
{
var oAuthHandlerProvider = ctx.HttpContext.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
var handler = await oAuthHandlerProvider.GetHandlerAsync(ctx.HttpContext, "Cookie");
var result = await handler!.AuthenticateAsync();
if (!result.Succeeded)
{
ctx.Fail("Faild Authentication");
return;
}
var token = ctx.AccessToken;
var user = result.Principal;
ctx.Principal = user.Clone();
ctx.Principal.Identities.First(x => x.AuthenticationType == "Cookie")
.AddClaim(new Claim("github-token", "tokenExists"));
};
});
Here the users session is successfully resolved and everything is fine. So I think that the problem is in my OAuth Authorization server. Do you have any idea why it couldn't resolve the user session?
I've tried to resolve the initial user session in OnTicketCreating event using IAuthenticationHandlerProvider.