C
C#14mo ago
OptoCloud

❔ Which OAuth packages to choose for 3rd party user authentication?

There is three collections of nuget packages that i can see: Owin.Security.Providers.[provider] AspNet.Security.OAuth.[provider] Microsoft.AspNetCore.Authentication.[provider] I want the users to be able to authenticate by using their discord/twitter/google account, and my frontend is using SvelteKit so everything is just pure json api's
47 Replies
0day
0day14mo ago
@OptoCloud r u there
OptoCloud
OptoCloud14mo ago
yup
0day
0day14mo ago
use these packages Owin.Security.Providers.Discord: This package provides OAuth authentication support for Discord. It allows users to authenticate with their Discord account. Owin.Security.Providers.Twitter: This package provides OAuth authentication support for Twitter. It allows users to authenticate with their Twitter account. Owin.Security.Google: This package provides OAuth authentication support for Google. It allows users to authenticate with their Google account.
OptoCloud
OptoCloud14mo ago
we have a chatgpt user here stare
0day
0day14mo ago
?? its literally from google you can literally google it and find the answer yourself
OptoCloud
OptoCloud14mo ago
i know not to use those because they don't even work liek it doesnt compile like severly outdated
0day
0day14mo ago
then use other packages
OptoCloud
OptoCloud14mo ago
thats the exact response chatgpt gave me it just assumed owin becuase of some info out there but later corrected itself once it realized it was outdated
0day
0day14mo ago
you do realise chatgpt extracts its solutions from google right ? right ?
OptoCloud
OptoCloud14mo ago
yup
0day
0day14mo ago
then whats the issue whats your front end then ? what is it using
OptoCloud
OptoCloud14mo ago
sveltekit
0day
0day14mo ago
json ?
OptoCloud
OptoCloud14mo ago
json
0day
0day14mo ago
oh idk then what both of them ?
OptoCloud
OptoCloud14mo ago
sveltekit that requests json from backend
0day
0day14mo ago
ok makes sense did you specify the client ID?
Henkypenky
Henkypenky14mo ago
for google you can use Microsoft.AspNetCore.Authentication.Google
0day
0day14mo ago
i tried it
Henkypenky
Henkypenky14mo ago
Microsoft.AspNetCore.Authentication.Twitter twitter
0day
0day14mo ago
it does not provide the right package
Henkypenky
Henkypenky14mo ago
AspNet.Security.OAuth.Discord 7.0.2
ASP.NET Core security middleware enabling Discord authentication.
Henkypenky
Henkypenky14mo ago
this looks good for discord
OptoCloud
OptoCloud14mo ago
this is where i start to wonder, why the name change one is aspnet the other is aspnetcore can both work or different use cases?
0day
0day14mo ago
prob because of legal reasons
Henkypenky
Henkypenky14mo ago
who is the op here
OptoCloud
OptoCloud14mo ago
me
0day
0day14mo ago
yes both can work
OptoCloud
OptoCloud14mo ago
Henkypenky
Henkypenky14mo ago
one is official from microsoft
0day
0day14mo ago
you can see the sticker
Henkypenky
Henkypenky14mo ago
the other is not
OptoCloud
OptoCloud14mo ago
but both are published to nuget by ms?
Henkypenky
Henkypenky14mo ago
no
0day
0day14mo ago
nope
OptoCloud
OptoCloud14mo ago
oh
Henkypenky
Henkypenky14mo ago
if it's Microsoft.* it's from microsoft
OptoCloud
OptoCloud14mo ago
i was thinking this
Henkypenky
Henkypenky14mo ago
aspnetcore
OptoCloud
OptoCloud14mo ago
Thanks, figured out the most of it now although some stuff is still really weird Will send that later But getting bunch of cors errors
Henkypenky
Henkypenky14mo ago
cors error are to be expected you need to configure it properly
OptoCloud
OptoCloud14mo ago
ok so i do this atm
services.AddAuthentication(OptoCloud.MyAuthScheme)
.AddOptoMiddleware(configuration)
.AddDiscord("discord", opt =>
{
opt.ClientId = configuration.GetValue<string>("Discord:OAuth2:ClientId")!;
opt.ClientSecret = configuration.GetValue<string>("Discord:OAuth2:ClientSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/discord";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.Scope.Add("email");
opt.Scope.Add("identify");
opt.Prompt = "none";
opt.SaveTokens = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<AuthenticationProperties>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? avatar = json.GetString("avatar");
if (avatar is null)
{
return null;
}
return $"https://cdn.discordapp.com/avatars/{json.GetString("id")}/{avatar}.png";
});
opt.Validate();
})
.AddGitHub("github", opt =>
{
opt.ClientId = configuration.GetValue<string>("GitHub:OAuth2:ClientId")!;
opt.ClientSecret = configuration.GetValue<string>("GitHub:OAuth2:ClientSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/github";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.Scope.Add("read:user");
opt.Scope.Add("user:email");
opt.SaveTokens = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<AuthenticationProperties>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? gravatarId = json.GetString("gravatar_id");
string? avatarUrl = json.GetString("avatar_url");

if (gravatarId is null) return avatarUrl;
if (avatarUrl is null) return $"https://www.gravatar.com/avatar/{gravatarId}?s=256";

return $"https://www.gravatar.com/avatar/{gravatarId}?s=256?d={avatarUrl}";
});
opt.Validate();
})
.AddTwitter("twitter", opt =>
{
opt.ConsumerKey = configuration.GetValue<string>("Twitter:OAuth1:ConsumerKey")!;
opt.ConsumerSecret = configuration.GetValue<string>("Twitter:OAuth1:ConsumerSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/twitter";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.SaveTokens = true;
opt.RetrieveUserDetails = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<RequestToken>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? profileImageUrl = json.GetString("profile_image_url_https");
if (profileImageUrl is null) return null;
return profileImageUrl.Replace("_normal", "_400x400");
});
opt.Validate();
})
.AddGoogle("google", opt =>
{
opt.ClientId = configuration.GetValue<string>("Google:OAuth2:ClientId")!;
opt.ClientSecret = configuration.GetValue<string>("Google:OAuth2:ClientSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/google";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
opt.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
opt.Scope.Add("openid");
opt.SaveTokens = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<AuthenticationProperties>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? picture = json.GetString("picture");
if (picture is null) return null;
return picture.Replace("s96-c", "s400-c");
});
opt.Validate();
});
services.AddAuthentication(OptoCloud.MyAuthScheme)
.AddOptoMiddleware(configuration)
.AddDiscord("discord", opt =>
{
opt.ClientId = configuration.GetValue<string>("Discord:OAuth2:ClientId")!;
opt.ClientSecret = configuration.GetValue<string>("Discord:OAuth2:ClientSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/discord";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.Scope.Add("email");
opt.Scope.Add("identify");
opt.Prompt = "none";
opt.SaveTokens = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<AuthenticationProperties>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? avatar = json.GetString("avatar");
if (avatar is null)
{
return null;
}
return $"https://cdn.discordapp.com/avatars/{json.GetString("id")}/{avatar}.png";
});
opt.Validate();
})
.AddGitHub("github", opt =>
{
opt.ClientId = configuration.GetValue<string>("GitHub:OAuth2:ClientId")!;
opt.ClientSecret = configuration.GetValue<string>("GitHub:OAuth2:ClientSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/github";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.Scope.Add("read:user");
opt.Scope.Add("user:email");
opt.SaveTokens = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<AuthenticationProperties>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? gravatarId = json.GetString("gravatar_id");
string? avatarUrl = json.GetString("avatar_url");

if (gravatarId is null) return avatarUrl;
if (avatarUrl is null) return $"https://www.gravatar.com/avatar/{gravatarId}?s=256";

return $"https://www.gravatar.com/avatar/{gravatarId}?s=256?d={avatarUrl}";
});
opt.Validate();
})
.AddTwitter("twitter", opt =>
{
opt.ConsumerKey = configuration.GetValue<string>("Twitter:OAuth1:ConsumerKey")!;
opt.ConsumerSecret = configuration.GetValue<string>("Twitter:OAuth1:ConsumerSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/twitter";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.SaveTokens = true;
opt.RetrieveUserDetails = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<RequestToken>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? profileImageUrl = json.GetString("profile_image_url_https");
if (profileImageUrl is null) return null;
return profileImageUrl.Replace("_normal", "_400x400");
});
opt.Validate();
})
.AddGoogle("google", opt =>
{
opt.ClientId = configuration.GetValue<string>("Google:OAuth2:ClientId")!;
opt.ClientSecret = configuration.GetValue<string>("Google:OAuth2:ClientSecret")!;
opt.CallbackPath = "/api/v1/auth/o/cb/google";
opt.AccessDeniedPath = "/api/v1/auth/o/denied";
opt.Scope.Add("https://www.googleapis.com/auth/userinfo.email");
opt.Scope.Add("https://www.googleapis.com/auth/userinfo.profile");
opt.Scope.Add("openid");
opt.SaveTokens = true;
opt.StateDataFormat = new DistributedCacheSecureDataFormat<AuthenticationProperties>();
opt.ClaimActions.MapCustomJson(OptoClaimTypes.ProfileImage, json =>
{
string? picture = json.GetString("picture");
if (picture is null) return null;
return picture.Replace("s96-c", "s400-c");
});
opt.Validate();
});
Extension method:
public static AuthenticationBuilder AddOptoMiddleware(this AuthenticationBuilder builder, IConfiguration configuration)
{
builder.Services.Configure<AuthenticationOptions>(o =>
{
o.AddScheme(OptoCloud.MyAuthScheme, scheme =>
{
scheme.HandlerType = typeof(OptoAuthenticationHandler);
scheme.DisplayName = null; // TODO: changeme
});
});

builder.Services.AddTransient<IAuthenticationSignInHandler, OptoAuthenticationHandler>();
return builder;
}
public static AuthenticationBuilder AddOptoMiddleware(this AuthenticationBuilder builder, IConfiguration configuration)
{
builder.Services.Configure<AuthenticationOptions>(o =>
{
o.AddScheme(OptoCloud.MyAuthScheme, scheme =>
{
scheme.HandlerType = typeof(OptoAuthenticationHandler);
scheme.DisplayName = null; // TODO: changeme
});
});

builder.Services.AddTransient<IAuthenticationSignInHandler, OptoAuthenticationHandler>();
return builder;
}
problem is after any oauth flow has finished creating its ticket it calls my authentication middleware, where the logic within it is not supposed to handle that kinda data so in the SignInAsync method in there i do this:
string? authenticationType = claimsIdentity.Identity?.AuthenticationType;
if (String.IsNullOrEmpty(authenticationType)) throw new InvalidOperationException($"Cannot sign in with an empty {nameof(claimsIdentity.Identity.AuthenticationType)}.");

SessionEntity session;
if (authenticationType == _scheme.Name)
{
session = (claimsIdentity as OptoPrincipal)!.Identity.Session;
}
else
{
session = await OAuthHandlers.HandleSignInAsync(authenticationType, claimsIdentity, properties, _dbContext, _logger);
}
string? authenticationType = claimsIdentity.Identity?.AuthenticationType;
if (String.IsNullOrEmpty(authenticationType)) throw new InvalidOperationException($"Cannot sign in with an empty {nameof(claimsIdentity.Identity.AuthenticationType)}.");

SessionEntity session;
if (authenticationType == _scheme.Name)
{
session = (claimsIdentity as OptoPrincipal)!.Identity.Session;
}
else
{
session = await OAuthHandlers.HandleSignInAsync(authenticationType, claimsIdentity, properties, _dbContext, _logger);
}
and then all this:
OptoCloud
OptoCloud14mo ago
OptoCloud
OptoCloud14mo ago
so short: client calls oauth init endpoint client gets redirected to oauth provider cient gets redirected to callback endpoint callback handler handles the response and gets the secrets, fetches the necessary info, creates a ticket then the ticket the remoteauthenticationhandler created gets passed into my main authenticationhandler main authenticationhandler checks if the authenticationscheme passed into it matches the expected If not it calls some custom oauth static methods i wrote which will parse that data and do some db calls to create the user if it doesnt exist, then return that to the authentcation handler @Henkypenky this seems overly complicated can't i just have my own authenticationhandler just for the oauth, seperate from the main one?
Henkypenky
Henkypenky14mo ago
authentication is complicated if it works let it be btw that looks pretty good if you ask me, maybe someone else knows how to do it better
Accord
Accord14mo ago
Was this issue resolved? If so, run /close - otherwise I will mark this as stale and this post will be archived until there is new activity.
OptoCloud
OptoCloud14mo ago
@Henkypenky
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;

server_name api.optocloud.no;

location / {
proxy_pass http://10.0.0.3:7296/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_pass_request_headers on;
}

ssl_certificate /etc/ssl/cloudflare/optocloud.no.cert.pem;
ssl_certificate_key /etc/ssl/cloudflare/optocloud.no.key.pem;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;

server_name api.optocloud.no;

location / {
proxy_pass http://10.0.0.3:7296/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_pass_request_headers on;
}

ssl_certificate /etc/ssl/cloudflare/optocloud.no.cert.pem;
ssl_certificate_key /etc/ssl/cloudflare/optocloud.no.key.pem;
}
my app is behind that and when it tries to do the callback the oauth2 tries to call back to: http://api.optocloud.no/api/v1/auth/o/cb/google http, not https im trying to get it to do https but it doesnt want to