How to use JwtBearer + SSO .NET 6 - asp.net

I search for similar questions but none responded what I needed so.
In my .net 6 app I can authenticate using normal JwtBearer like
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(configureOptions =>
{
configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
configureOptions.TokenValidationParameters = tokenValidationParameters;
configureOptions.SaveToken = true;
});
and I can search my database or external api to decide claims to be included in the token creation process, that starts in a “api/auth/login” endpoint.
It work great with a username and pwd as arguments.
Now I need to add SSO using Azure AD.
I can add authentication easily with
services.AddMicrosoftIdentityWebAppAuthentication(configuration);
and if I do an
app.Use(async (context, next) =>
{
if (context.User.Identity is not null && !context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync();
}
else
{
await next();
}
});
I can see the username, email and AzureId (not at the same time I do the challenge, but in the next request.. must wait the next() middleware I guess..)
What I want is to use both at the same time and in the token creation process I want to use AzureId and Azure username instead of a username and pwd parameter in the endpoint. If I add the AddMicrosoftIdentityWebAppAuthentication the authentication based on claims don`t work (I would like to keep the default Authorize attribute or replace the default so I can use the minimal api without using [CustomAuthorize(…)], I want to use the fluent way like
app.MapGet("someendpoint", async (IMediator mediator) =>
{
return (await mediator.Send(new SomeRequest { })).Result;
})
.RequireAuthorization("BlaBlaBla");
I don`t mind creating dynamic policies.

Related

ASP.NET - Problem with authentication: Manually added claim is missing in the next request

I am currently experimenting with external login providers like Google Authentication in my ASP.NET application.
As you can see on my Program.cs i'am running .NET6.
After the Google-Login was successfull, the ClaimsPrincipal has exactly one ClaimsIdentity (IsAuthenticated == true). Now I want to add my own 'Custom-Claim' to this identity. The addition works without any problems, but the next request is missing the custom claim (all other claims by Google are there).
Here is the part of my Program.cs where I add the authentication:
builder.Services.AddAuthentication(x =>
{
x.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogle(GoogleDefaults.AuthenticationScheme, o =>
{
o.ClientId = builder.Configuration["Authentication:Google:ClientId"];
o.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
o.ClaimActions.MapJsonKey("urn:google:picture","picture","url");
});
Here is the configuration of the middleware pipeline in Program.cs:
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.Use((httpContext, func) =>
{
if (httpContext.Request.Path.StartsWithSegments("/api"))
httpContext.Request.Headers[HeaderNames.XRequestedWith] = "XMLHttpRequest";
return func();
});
app.UseRouting();
app.MapRazorPages();
app.MapControllers();
Endpoint for Google Login:
[HttpGet]
[Route("GoogleSignIn")]
public async Task GoogleSignIn()
{
await HttpContext.ChallengeAsync(GoogleDefaults.AuthenticationScheme,
new AuthenticationProperties {RedirectUri =Url.Action("GoogleResponse")});
}
In the "GoogleResponse" method i add the mentioned custom claim:
//this method gets called after the google login has finished
public async Task<IActionResult> GoogleResponse()
{
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var identity = result.Principal.Identities.FirstOrDefault();
var currentSidClaim = identity.FindFirst(x => x.Type == ClaimTypes.Sid);
if (currentSidClaim != null)
identity.RemoveClaim(currentSidClaim);
identity.AddClaim(new Claim(ClaimTypes.Sid, "Hi i am some claim, but i'll miss on the next request :("));
return Redirect("/");
}
ClaimsIdentity after adding the claim:
[https://i.stack.imgur.com/INdGy.png]
ClaimsIdentity on the next request (in the same controller btw):
[https://i.stack.imgur.com/oft3e.png]
Whether I access the identity via "User.Identity" or "HttpContext.User..." makes no difference.
Really hope somebody can clear things up for me.
I hope I don't have to implement a large / elaborate ASP.NET Identity solution to solve the problem. I would be happy with a lightweight authentication.
UPDATE:
After some further tests it looks like it is not necessarily related to the specific Google login. Even if I create a second identity at login, it is gone at the next request.
I guess the authentication cookie is not updated.
Thanks in advance!
Try to add claims after callback from the external website. As described here

Migrating salted sha512 passwords from symfony 2 to firebase authentication

I am trying to migrate users (including passwords) from an old symfony 2 application to firebase authentication (or google identity platform).
In the symfony2 application the passwords of the users are hashed using sha512 with a salt. I already found that users can be imported using their password and hash in the documentation of firebase (https://firebase.google.com/docs/auth/admin/import-users). However it seems like the sha512 hashing that is used by firebase is not the same as was used by symfony.
For the old symfony project the following configuration is used:
security:
encoders:
FOS\UserBundle\Model\UserInterface: sha512
By looking into the source I found that symfony given a salt and a password symfony will produce the hash like this: (in python code)
def get_hash(salt, password):
hash = password.encode('utf-8')
salted = hash + salt
hash = hashlib.sha512(salted).digest()
for i in range(1, 5000):
# symfony keeps adding salted for every iteration, this is something firebase does not it seems
hash = hashlib.sha512(hash + salted).digest()
return base64.b64encode(hash).decode('utf-8')
However this code does not allow me to login when i import it like in the code below. It however does produce the same hash as I have in my database of the symfony2 application:
app = firebase_admin.initialize_app()
salt = '{test}'.encode('utf-8')
hash = get_hash(salt=salt, password='xyz')
print('calculated hash', base64.b64encode(hash))
users = [
auth.ImportUserRecord(
uid='foobar',
email='foo#bar.com',
password_hash=hash,
password_salt=salt
)
]
hash_alg = auth.UserImportHash.sha512(rounds=5000)
try:
result = auth.import_users(users, hash_alg=hash_alg)
for err in result.errors:
print('Failed to import user:', err.reason)
except exceptions.FirebaseError as error:
print('Error importing users:', error)
I can however login with the password when i use the following fuction.
def get_hash(salt, password):
hash = password.encode('utf-8')
salted = salt + hash
hash = hashlib.sha512(salted).digest()
for i in range(1, 5000):
hash = hashlib.sha512(hash).digest()
return hash
I have already found a way to change the order of adding the salt but i can find no way to hash like this in firebase hash = hashlib.sha512(hash + salted).digest().
Now it seems like there is no way to migrate my password to firebase as the implementation of symfony is a bit different from the one used by firebase. Does anyone know a way to make sure I can still import my current hashes? This would be great.
If not, what would be alternative work arounds?
Is it possible to let firebase do a request to my own endpoint to verify password.
Another way would be to try to catch the signin process and send it to my own endpoint first, set the password in the background and then send the request to firebase?
You haven't specified what your client application is using, so I'm just going to assume it's a web application that will use the Firebase Web SDK.
To use this solution, you'll need to migrate the Symfony user data to Firestore under a private _migratedSymfonyUsers collection, where each document is the email of that user.
On the client, the process will be:
Collect email and password from the user
Attempt to sign in to Firebase with that email and password combination
If that failed, invoke a Callable Cloud Function with that email and password combination.
If function returned a success message (see below), reattempt signing in the user with the given email and password
Handle success/errors as appropriate
On the client, this would look like:
const legacySignIn = firebase.functions().httpsCallable('legacySignIn');
async function doSignIn(email, password) {
try {
return await firebase.auth()
.signInWithEmailAndPassword(email, password);
} catch (fbError) {
if (fbError.code !== "auth/user-not-found")
return Promise.reject(fbError);
}
// if here, attempt legacy sign in
const response = await legacySignIn({ email, password });
// if here, migrated successfully
return firebase.auth()
.signInWithEmailAndPassword(email, password);
}
// usage:
doSignIn(email, password)
.then(() => console.log('successfully logged in/migrated'))
.catch((err) => console.error('failed to log in', err));
In the Callable Cloud Function:
(optional) Assert that the request is coming from your application with App Check
Assert email and password were provided and throw error if not.
Assert that the email given exists in your migrated users and throw an error if not.
If in migrated users, hash the password and compare against the stored hash.
Throw an error if hashes don't match.
If hashes match, create a new Firebase user with that email and password combination
Once created, delete the migrated hash and return success message to the caller
On the server, this would look like:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
function symfonyHash(pwd, salt) {
// TODO: Hash function
return /* calculatedHash */;
}
exports.legacySignIn = functions.https.onCall(async (data, context) => {
if (context.app == undefined) { // OPTIONAL
throw new functions.https.HttpsError(
'failed-precondition',
'The function must be called from an App Check verified app.');
}
if (!data.email || !data.password) {
throw new functions.https.HttpsError(
'invalid-argument',
'An email-password combination is required');
}
if (data.email.indexOf("/") > -1) {
throw new functions.https.HttpsError(
'invalid-argument',
'Email contains forbidden character "/"');
}
const migratedUserSnapshot = await admin.firestore()
.doc(`_migratedSymfonyUsers/${data.email}`);
if (!migratedUserSnapshot.exists) {
throw new functions.https.HttpsError(
'not-found',
'No user matching that email address was found');
}
const storedHash = migratedUserSnapshot.get("hash");
const calculatedHash = symfonyHash(password, salt);
if (storedHash !== calculatedHash) {
throw new functions.https.HttpsError(
'permission-denied',
'Given credential combination doesn\'t match');
}
// if here, stored and calculated hashes match, migrate user
// get migrated user data
const { displayName, roles } = migratedUserSnapshot.data();
// create the user based on migrated data
const newUser = await admin.auth().createUser({
email,
password,
...(displayName ? { displayName } : {})
});
if (roles) { // <- OPTIONAL
const roleMap = {
"symfonyRole": "tokenRole",
"USERS_ADMIN": "isAdmin",
// ...
}
const newUserRoles = [];
roles.forEach(symfonyRole => {
if (roleMap[symfonyRole]) {
newUserRoles.push(roleMap[symfonyRole]);
}
});
if (newUserRoles.length > 0) {
// migrate roles to user's token
await setCustomUserClaims(
newUser.uid,
newUserRoles.reduce((acc, r) => { ...acc, [r]: true }, {})
);
}
}
// remove the old user data now that we're done with it.
await hashSnapshot.ref.delete();
// return success to client
return { success: true };
});

How to use my firebase authentication to work with external services?

Ok so I am using firebase as authentication for my iOS app. Now I plan on adding video calling to my app using an external service know as connectyCube. This service has their own authentication system and I cannot use their services unless a user is authenticated.
Option 1: I can use their own authentication which means my app would have two authentication systems - not very productive
Option 2: They say I can use an existing authentication to validate users
I understand that this is a common thing in the developers world and I see the word OAuth and JWT being thrown around but I am a rookie developer and I want to understand how I can use firebase and authenticate a user from an external service.
These are the questions they have asked when I opted for the "I have my own authentication" option:
What is your end point URL
Is it GET or POST
Request Headers
Request Params
Response Params
Where do I get all this information from firebase? Any help would be great
As an alternative to #Dharmaraj's answer, you could instead make use of a HTTP Event Cloud Function for this based on the code sample they've provided.
Using this method, you create the endpoint /verifyUserToken to be used by ConnectyCube.
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
export const verifyUserToken = functions.https.onRequest((req, res) => {
const idToken = req.query.token;
verifyUser(idToken)
.then(
(userData) => {
res.status(200).json(userData)
},
(err) => {
console.log("Token verification failed.", err.code || err.message);
res.status(422).json({error: "User token is invalid"})
}
)
.catch((err) => console.error("Unexpected crash", err));
});
async function verifyUser(token) {
if (!token)
throw new Error("token missing");
// using `true` here to force token to be checked against the Firebase
// Auth API rather than trusting its contents as-is
const { uid, email } = await admin.auth().verifyIdToken(token, true);
// pull the user's username from their user data
// at /users/{userId}/username
const username = (await admin.database().ref("users/" + uid + "/username")).val();
// use user's actual email if available, otherwise fallback
// to a userID based email
const uEmail = email || uid + "#users.noreply.yourapp.com";
// use user's username if available, otherwise fallback to
// the email address above.
const uLogin = username !== null ? username : uEmail;
return {
uid,
login: uLogin,
email: uEmail,
user: {id: uid, login: uLogin, email: uEmail}, // <- this part in particular is used by ConnectyCube
users: [{uid, login: uLogin, email: uEmail}]
};
}
Once deployed, you would use the following settings:
Setting
Value
API URL:
https://us-central1-PROJECT-ID.cloudfunctions.net/verifyUserToken
GET/POST
GET
Request params:
{"token": "#{login}"}
Response params:
{"uid": "#{user.id}", "email": #{user.email}, "login": "#{user.login}"}
It looks like ConnectyCube uses some sort of Session Tokens as mentioned in their documentation with their own username and password.
The most easiest way would be creating a ConnectyCube account whenever a new user signs up in your Firebase app using Firebase Auth Triggers for Cloud functions. Then you can generate username and password on behalf of your user and store them in a Database.
So whenever you need to create a ConnectyCube session, check for the currently logged in user and fetch their ConnectyCube credentials.
async function createCCSession() {
const userId = firebase.auth().currentUser.uid
const ccCrednetials = (await firebase.database().ref(`ccCreds/${userId}`).once('value')).val()
ConnectyCube.createSession(ccCredentials)
.then((session) => {
console.log(session)
return session
}).catch((error) => console.log(error));
}
You can protect the database using security rules so a user can access their credentials only.
{
"rules": {
"ccCreds": {
"$uid": {
".read": "$uid === auth.uid"
}
}
}
}
While I don't normally double-answer a question, in the course of exploring some other authentication related problems, I've managed to eliminate the Cloud Function from my other answer entirely and instead call the Authentication API directly.
Setting
Value
API URL:
https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo?key=FIREBASE_CONFIG_API_KEY
GET/POST
POST
Request params:
{"idToken": "#{login}"}
Response params:
{"uid": "#{users.0.localId}", "email": #{users.0.email}, "full_name": "#{users.0.displayName}"}
On your client, you just call the ConnectyCube Login API with the following data:
POST https://api.connectycube.com/login
login=<Firebase-ID-token>
password=<any-random-value-to-pass-the-validation>

how to Authorize openiddict token generated from localhost/UserManagement API in other API localhost/SalesAPI

I have created openiddict token in dot net core api and that application is hosted at localhost/UserManagementAPI. When I try to authorize same API then I am able to do it. But when I try to use same token and Authorize other API localhost/SalesAPI it gives me unauthorized access error.
Token generation code is as below UserManagementAPI/startup.cs
services.AddAuthentication().AddOpenIdConnectServer(options =>
{
options.TokenEndpointPath = "/authorize";
options.AllowInsecureHttp = true;
options.Provider.OnValidateTokenRequest = context =>
{
if (!context.Request.IsPasswordGrantType() && !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
if (string.IsNullOrEmpty(context.ClientId))
{
context.Skip();
return Task.CompletedTask;
}
if (string.Equals(context.ClientId, "client_id", StringComparison.Ordinal) &&
string.Equals(context.ClientSecret, "client_secret", StringComparison.Ordinal))
{
context.Validate();
}
return Task.CompletedTask;
};
options.Provider.OnHandleTokenRequest = context =>
{
if (context.Request.IsPasswordGrantType())
{
if (!string.Equals(context.Request.Username, "testusername", StringComparison.Ordinal) ||
!string.Equals(context.Request.Password, "testpassword", StringComparison.Ordinal))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid user credentials.");
return Task.CompletedTask;
}
var identity = new ClaimsIdentity(context.Scheme.Name,
OpenIdConnectConstants.Claims.Name,
OpenIdConnectConstants.Claims.Role);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, Guid.NewGuid().ToString());
identity.AddClaim("userid", "1001",
OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Scheme.Name);
ticket.SetAccessTokenLifetime(TimeSpan.FromDays(1));
ticket.SetScopes(OpenIdConnectConstants.Scopes.Profile);
context.Validate(ticket);
}
return Task.CompletedTask;
};
});
I have added below code to validate token in localhost/SalesAPI startup.cs
services.AddOpenIddict();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Bearer";
options.DefaultChallengeScheme = "Bearer";
}).AddOAuthValidation();
I dont want to use authorization server.
with above code I am able to authorize other api's from localhost/UserManagementAPI (same api is responsible to generate a token)
please let me know If I am missing something here.
It's worth noting you're not using OpenIddict in this snippet, but AspNet.Security.OpenIdConnect.Server, the low-level OpenID Connect server middleware that powers OpenIddict 1.x and 2.x.
If your resource server is located in a separate application, you'll need to configure that application to use the same ASP.NET Core Data Protection keys as the main application. Take a look at ASOS - Token validation is not working when having separate authorization server and the resource server for more information on how to do that.

How to make [Authorize] trigger openId middleware

I'm trying out some features of ASP.NET 5 and I'm struggling a bit with authentication. I've managed to use most of this sample app to connect to my Azure AD to log in, but I can't figure out how to restrict parts of my web app to authenticated users only. The article that accompanies the sample app I used states that
You can trigger the middleware to send an OpenID Connect sign-in
request by decorating a class or method with the [Authorize]
attribute, or by issuing a challenge
Since I'd like to avoid repeating the same challenge code everywhere, I opted for the attribute approach, but it doesn't work at all. All it seems to do is block access to unauthorized users, without redirecting to the login page the way the challenge does.
Since I intended the app I am building to be more private than public, I've also tried creating a global policy and opening up some select features using the AllowAnonymous attribute. This works, but again the unauthorized pages are simply shown as blank, instead of a challenge being issued.
This is the policy code I'm using currently, taken from here:
var policy = new AuthorizationPolicyBuilder()
//This is what makes it function like the basic [Authorize] attribute
.RequireAuthenticatedUser()
.Build();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new AuthorizeFilter(policy));
});
Am I missing some setup to the authorization attribute or the policy that issues the challenge?
For posterity and most likely my future self as well:
I was missing the AutomaticAuthentication property in the OpenIdConnectOptions. The sample app was set up like this:
// Configure the OWIN Pipeline to use Cookie Authentication
app.UseCookieAuthentication(options =>
{
// By default, all middleware are passive/not automatic. Making cookie middleware automatic so that it acts on all the messages.
options.AutomaticAuthentication = true;
});
// Configure the OWIN Pipeline to use OpenId Connect Authentication
app.UseOpenIdConnectAuthentication(options =>
{
options.ClientId = Configuration.Get("AzureAd:ClientId");
options.Authority = String.Format(Configuration.Get("AzureAd:AadInstance"), Configuration.Get("AzureAd:Tenant"));
options.PostLogoutRedirectUri = Configuration.Get("AzureAd:PostLogoutRedirectUri");
options.Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
};
});
To get everything to work I had to make small adaptations to make it look like this:
app.UseCookieAuthentication(options => { options.AutomaticAuthentication = true; });
// Configure the OWIN Pipeline to use OpenId Connect Authentication
app.UseOpenIdConnectAuthentication(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.ClientId = Configuration.Get("AzureAd:ClientId");
options.Authority = String.Format(Configuration.Get("AzureAd:AadInstance"), Configuration.Get("AzureAd:Tenant"));
options.PostLogoutRedirectUri = Configuration.Get("AzureAd:PostLogoutRedirectUri");
options.Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
};
options.AutomaticAuthentication = true;
});

Resources