Asp.net mvc 5; Where to add custom claims? (adfs login) - asp.net-mvc

We have a mvc 5 project set up with adfs (ws-federation) authentication. Now, we want to add a custom claim to the user if a flag is true in the db. Where would be the correct place to do this?

This blog entry got us in the right direction. The claims need to be injected at the last minute after the user is validated.
Startup.Auth.cs should look something like this:
app.UseActiveDirectoryFederationServicesBearerAuthentication(
new ActiveDirectoryFederationServicesBearerAuthenticationOptions
{
MetadataEndpoint = ConfigurationManager.AppSettings["ida:AdfsMetadataEndpoint"],
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"],
//NameClaimType = "User-Principal-Name",
//SaveSigninToken = true
},
//Inject custom claims from Database
Provider = new OAuthBearerAuthenticationProvider()
{
OnValidateIdentity = async context =>
{
string UPN = context.Ticket.Identity.FindFirst(ClaimTypes.Upn).Value;
UPN = UPN.Remove(UPN.Length - 12);
User user = new User();
//user = GetUserData("user#domain.com");
user = GetUserData(UPN); //Get user data from your DB
context.Ticket.Identity.AddClaim(
new Claim("UserName", user.UserName.ToString(), ClaimValueTypes.String, "LOCAL AUTHORITY"));
}
}
});

The easiest way is to set up a SQL attribute store and then write a custom rule to query the store.
As per the article, something like:
c:[type == "http://contoso.com/emailaddress"]
=> issue (store = "Custom SQL Store", types = ("http://contoso.com/age", "http://contoso.com/purchasinglimit"), query = "SELECT age,purchasinglimit FROM users WHERE email={0}",param = c.value);

Related

The given token is invalid error in EWS OAuth authentication when using personal account

I have to get the contacts from Exchange server from any account, so we have used the code from below link.
https://learn.microsoft.com/en-us/exchange/client-developer/exchange-web-services/how-to-authenticate-an-ews-application-by-using-oauth
But it is not working for personal accounts, which is working fine for our organization account. So I have used AadAuthorityAudience property instead of TenantId and changed the scope from EWS.AccessAsUser.All to others. Now authentication got success but getting "The given token is invalid" error while using the token in ExchangeService.
var pcaOptions = new PublicClientApplicationOptions {
ClientId = "77xxxxxxxxxxx92324",
//TenantId = "7887xxxxxxxxxxxxx14",
RedirectUri = "https://login.live.com/oauth20_desktop.srf",
AadAuthorityAudience = AadAuthorityAudience.AzureAdAndPersonalMicrosoftAccount};
var pca = PublicClientApplicationBuilder.CreateWithApplicationOptions(pcaOptions).Build();
//var ewsScopes = new string[] { "https://outlook.office365.com/EWS.AccessAsUser.All" };
var ewsScopes = new string[] { "User.Read", "Contacts.ReadWrite.Shared" };
var authResult = await pca.AcquireTokenInteractive(ewsScopes).ExecuteAsync();
var ewsClient = new ExchangeService();
ewsClient.Url = new Uri("https://outlook.office365.com/EWS/Exchange.asmx");
//ewsClient.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddress, "araj#concord.net");
ewsClient.Credentials = new OAuthCredentials(authResult.AccessToken);
// Make an EWS call
var folders = ewsClient.FindFolders(WellKnownFolderName.MsgFolderRoot, new FolderView(10));
What am doing wrong here?
https://outlook.office365.com/EWS.AccessAsUser.All is the right scope to use. The scope is invalid for personal accounts since they're not supported by EWS.

Allow all domains when adding a user to Azure B2C using the Graph API

I am trying to add a user with the email ...#gmail.com to my B2C directory via the Graph API (C#). I get this as a response:
The domain portion of the userPrincipalName property is invalid. You
must use one of the verified domain names in your organization.
This system needs to allow for users of any email domain to sign in. The users need to log in to a website, not have access to the Azure Portal.
Is there a way to accomplish this without manually adding every domain?
Code for adding user via Graph API:
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var user = new User
{
AccountEnabled = true,
DisplayName = emailAddress,
MailNickname = emailAddress.Split('#').FirstOrDefault(),
UserPrincipalName = emailAddress,
PasswordProfile = new PasswordProfile
{
ForceChangePasswordNextSignIn = true,
Password = tempPassword
}
};
If you're trying to create local B2C (not AAD) accounts try setting the identities property in your request but not the upn. This last should be auto-generated. Also password expirations must be disabled, and force change password at next sign-in must also be disabled.
I had to add following packages:
<PackageReference Include="Microsoft.Graph" Version="4.0.0-preview.7" />
<PackageReference Include="Microsoft.Graph.Auth" Version="1.0.0-preview.7" />
Then:
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(Settings.ClientId)
.WithTenantId(Settings.Tenant)
.WithClientSecret(Settings.ClientSecret)
.Build();
var authProvider = new ClientCredentialProvider(confidentialClientApplication);
var graphClient = new GraphServiceClient(authProvider);
var user = new User
{
AccountEnabled = true,
GivenName = "Name",
Surname = "Surname",
DisplayName = "Name Surname",
PasswordProfile = new PasswordProfile
{
ForceChangePasswordNextSignIn = false,
Password = "pass.123",
},
PasswordPolicies = "DisablePasswordExpiration",
Identities = new List<ObjectIdentity>
{
new ObjectIdentity()
{
SignInType = "emailAddress",
Issuer = Settings.Tenant,
IssuerAssignedId = "sample#sample.com"
}
}
};
await graphClient.Users.Request().AddAsync(user);
Make sure to add permission to create users in Azure portal.

Invalid Access Token/Missing Claims when logged into IdentityServer4

I have a standard .NET Core 2.1 (MVC and API) and Identity Server 4 project setup.
I am using reference tokens instead of jwt tokens.
The scenario is as follows:
Browse to my application
Redirected to Identity Server
Enter valid valid credentials
Redirected back to application with all claims (roles) and correct access to the application and API
Wait an undetermined amount of time (I think it's an hour, I don't have the exact timing)
Browse to my application
Redirected to Identity Server
I'm still logged into the IDP so I'm redirected immediately back to my
application
At this point the logged in .NET user is missing claims (roles) and no longer has access to the API
The same result happens if I delete all application cookies
It seems obvious to me that the access token has expired. How do I handle this scenario? I'm still logged into the IDP and the middleware automatically logged me into my application, however, with an expired (?) access token and missing claims.
Does this have anything to do with the use of reference tokens?
I'm digging through a huge mess of threads and articles, any guidance and/or solution to this scenario?
EDIT: It appears my access token is valid. I have narrowed my issue down to the missing user profile data. Specifically, the role claim.
When I clear both my application and IDP cookies, everything works fine. However, after "x" (1 hour?) time period, when I attempt to refresh or access the application I am redirected to the IDP then right back to the application.
At that point I have a valid and authenticated user, however, I am missing all my role claims.
How can I configure the AddOpenIdConnect Middleware to fetch the missing claims in this scenario?
I suppose in the OnUserInformationReceived event I can check for the missing "role" claim, if missing then call the UserInfoEndpoint...that seems like a very odd workflow. Especially since on a "fresh" login the "role" claim comes back fine. (Note: I do see the role claim missing from the context in the error scenario).
Here is my client application configuration:
services.AddAuthentication(authOpts =>
{
authOpts.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
authOpts.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts => { })
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, openIdOpts =>
{
openIdOpts.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
openIdOpts.Authority = settings.IDP.Authority;
openIdOpts.ClientId = settings.IDP.ClientId;
openIdOpts.ClientSecret = settings.IDP.ClientSecret;
openIdOpts.ResponseType = settings.IDP.ResponseType;
openIdOpts.GetClaimsFromUserInfoEndpoint = true;
openIdOpts.RequireHttpsMetadata = false;
openIdOpts.SaveTokens = true;
openIdOpts.ResponseMode = "form_post";
openIdOpts.Scope.Clear();
settings.IDP.Scope.ForEach(s => openIdOpts.Scope.Add(s));
// https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/
// https://github.com/aspnet/Security/issues/1449
// https://github.com/IdentityServer/IdentityServer4/issues/1786
// Add Claim Mappings
openIdOpts.ClaimActions.MapUniqueJsonKey("preferred_username", "preferred_username"); /* SID alias */
openIdOpts.ClaimActions.MapJsonKey("role", "role", "role");
openIdOpts.TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = settings.IDP.ClientId,
ValidIssuer = settings.IDP.Authority,
NameClaimType = "name",
RoleClaimType = "role"
};
openIdOpts.Events = new OpenIdConnectEvents
{
OnUserInformationReceived = context =>
{
Log.Info("Recieved user info from IDP.");
// check for missing roles? they are here on a fresh login but missing
// after x amount of time (1 hour?)
return Task.CompletedTask;
},
OnRedirectToIdentityProvider = context =>
{
Log.Info("Redirecting to identity provider.");
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
Log.Debug("OnTokenValidated");
// this addressed the scenario where the Identity Server validates a user however that user does not
// exist in the currently configured source system.
// Can happen if there is a configuration mismatch between the local SID system and the IDP Client
var validUser = false;
int uid = 0;
var identity = context.Principal?.Identity as ClaimsIdentity;
if (identity != null)
{
var sub = identity.Claims.FirstOrDefault(c => c.Type == "sub");
Log.Debug($" Validating sub '{sub.Value}'");
if (sub != null && !string.IsNullOrWhiteSpace(sub.Value))
{
if (Int32.TryParse(sub.Value, out uid))
{
using (var configSvc = ApiServiceHelper.GetAdminService(settings))
{
try
{
var usr = configSvc.EaiUser.GetByID(uid);
if (usr != null && usr.ID.GetValueOrDefault(0) > 0)
validUser = true;
}
catch { }
}
}
}
Log.Debug($" Validated sub '{sub.Value}'");
}
if (!validUser)
{
// uhhh, does this work? Logout?
// TODO: test!
Log.Warn($"Unable to validate user is SID for ({uid}). Redirecting to '/Home/Logout'");
context.Response.Redirect("/Home/Logout?msg=User not validated in source system");
context.HandleResponse();
}
return Task.CompletedTask;
},
OnTicketReceived = context =>
{
// TODO: Is this necessary?
// added the below code because I thought my application access_token was expired
// however it turns out I'm actually misisng the role claims when I come back to the
// application from the IDP after about an hour
if (context.Properties != null &&
context.Properties.Items != null)
{
DateTime expiresAt = System.DateTime.MinValue;
foreach (var p in context.Properties.Items)
{
if (p.Key == ".Token.expires_at")
{
DateTime.TryParse(p.Value, null, DateTimeStyles.AdjustToUniversal, out expiresAt);
break;
}
}
if (expiresAt != DateTime.MinValue &&
expiresAt != DateTime.MaxValue)
{
// I did this to synch the .NET cookie timeout with the IDP access token timeout?
// This somewhat concerns me becuase I thought that part should be done auto-magically already
// I mean, refresh token?
context.Properties.IsPersistent = true;
context.Properties.ExpiresUtc = expiresAt;
}
}
return Task.CompletedTask;
}
};
});
I'm sorry folks, looks like I found the source of my issue.
Total fail on my side :(.
I had a bug in the ProfileService in my Identity Server implementation that was causing the roles to not be returned in all cases
humph, thanks!

Google API Calender v3 Event Insert via Service Account using Asp.Net MVC

I have been trying to insert a Google calendar event via Google service account that was created for an app in my dev console, but I am continually getting a helpless 404 response back on the Execute method. In the overview of the dev console I can see that the app is getting requests because there are instances of errors on the calendar.events.insert method. There is no information on what is failing. I need this process to use the Service account process instead of OAuth2 so as to not require authentication each time a calendar event needs to be created.
I have set up the service account, given the app a name, have the p12 file referenced in the project. I've also, gone into a personal calendar and have shared with the service account email address. Also, beyond the scope of this ticket, I have created a secondary app, through an administration account and have granted domain wide access to the service account only to receive the same helpless 404 error that this is now giving.
Error Message: Google.Apis.Requests.RequestError
Not Found [404]
Errors [Message[Not Found] Location[ - ] Reason[notFound] Domain[global]
Any help identifying a disconnect or error would be greatly appreciated.
var URL = #"https://www.googleapis.com/calendar/v3/calendars/testcalendarID.com/events";
string serviceAccountEmail = "createdserviceaccountemailaq#developer.gserviceaccount.com";
var path = Path.Combine(HttpRuntime.AppDomainAppPath, "Files/myFile.p12");
var certificate = new X509Certificate2(path, "notasecret",
X509KeyStorageFlags.Exportable);
ServiceAccountCredential credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = new[] { Google.Apis.Calendar.v3.CalendarService.Scope.Calendar },
}.FromCertificate(certificate));
BaseClientService.Initializer initializer = new
BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Test App"
};
Google.Apis.Calendar.v3.CalendarService calservice = new Google.Apis.Calendar.v3.CalendarService(initializer);
string timezone = System.TimeZone.CurrentTimeZone.StandardName;
var calendarEvent = new Event()
{
Reminders = new Event.RemindersData()
{
UseDefault = true
},
Summary = title,
Description = description,
Location = location,
Start = new EventDateTime()
{
//DateTimeRaw = "2014-12-24T10:00:00.000-07:00",
DateTime = startDateTime,
TimeZone = "America/Phoenix"
},
End = new EventDateTime()
{
//DateTimeRaw = "2014-12-24T11:00:00.000-08:00",
DateTime = endDateTime,
TimeZone = "America/Phoenix"
},
Attendees = new List<EventAttendee>()
{
new EventAttendee()
{
DisplayName = "Joe Shmo",
Email = "joeshmoemail#email.com",
Organizer = false,
Resource = false
}
}
};
var insertevent = calservice.Events.Insert(calendarEvent, URL);
var requestedInsert = insertevent.Execute();
I had the same problem. The solution was to add an email client, whose calendar event you want to send.
Credential = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(serviceAccountEmail)
{
Scopes = Scopes,
User = "example_client_email#gmail.com"
}.FromCertificate(certificate));
So I found out that for this to work, You need to make sure that you access the google.Admin account for referencing the service account Client ID of the app you created.
Another thing that helps is making sure the timezone is in the following format "America/Phoenix"
I have now successfully created events through the service account WITHOUT authentication.

How can I pass e-mail with Oauth MVC4

http://www.asp.net/mvc/tutorials/mvc-4/using-oauth-providers-with-mvc
I'm using code from this tutorial (of course, not all). Everything works perfectly, but when I tried to pass email, I have System.Collections.Generic.KeyNotFoundException. Why? How can I pass e-mail value from Facebook?
return View("ExternalLoginConfirmation", new RegisterExternalLoginModel {
UserName = result.UserName,
ExternalLoginData = loginData,
FullName = result.ExtraData["name"],
Email = result.ExtraData["email"],
ProfileLink = result.ExtraData["link"],
});
This works:
return View("ExternalLoginConfirmation", new RegisterExternalLoginModel {
UserName = result.UserName,
ExternalLoginData = loginData,
FullName = result.ExtraData["name"],
//Email = result.ExtraData["email"],
ProfileLink = result.ExtraData["link"],
});
Regards
Facebook doesn't share Email addresses by default. See this post for more information, but you can change your registration model to require email when making a user registration for your site. Also, you can check that the collection has the key first, before trying to access it
AuthenticationResult result =
OAuthWebSecurity
.VerifyAuthentication(
Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
// Log in code
if (result.ExtraData.ContainsKey("email"))
// Use email

Resources