How to implement Single Sign On in ASP.NET MVC application - sql-server

We have developed 2 applications in ASP.NET MVC. We have used ASP.NET Identity in both applications for user authentication. Both applications use the same database. Also both apps are hosted on Azure Portal.
We would like to implement SSO in both applications, so when I log in to one application, the second application doesn't ask for login again and authenticates the already logged in user.
We have implemented to share authentication cookies among ASP.NET apps as per the Microsoft document (open the link shared below), but it's not working. I have posted code below.
https://learn.microsoft.com/en-us/aspnet/core/security/cookie-sharing?view=aspnetcore-2.1#sharing-authentication-cookies-between-aspnet-4x-and-aspnet-core-applications
I have written this code in Startup.Auth.cs:
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
CookieName = ".AspNet.SharedCookie",
CookieDomain= ".azurewebsites.net",
SlidingExpiration = true,
ExpireTimeSpan = TimeSpan.FromMinutes(120),
LoginPath = PathString.FromUriComponent("/Account/Login"),
LogoutPath = PathString.FromUriComponent("/Account/Logout"),
TicketDataFormat = new AspNetTicketDataFormat(
new DataProtectorShim(
DataProtectionProvider.Create(new DirectoryInfo("C:\\ApplicationData"),
(builder) =>
{
builder.SetApplicationName("SharedCookieApp");
})
.CreateProtector(
"Microsoft.AspNetCore.Authentication.Cookies." +
"CookieAuthenticationMiddleware",
"Cookies.Application",
"v2"))),
CookieManager = new ChunkingCookieManager()
});
}
Also added claim when user logged in successfully in login method as below.
var user1 = await UserManager.FindByNameAsync(model.Email);
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user1.Id));
claims.Add(new Claim(ClaimTypes.Name, model.Email));
var id = new ClaimsIdentity(claims,DefaultAuthenticationTypes.ApplicationCookie);
var ctx = Request.GetOwinContext();
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = true,
ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
IssuedUtc = DateTime.UtcNow
}, id);
This works fine in localhost but when it is published in Azure portal, it doesn't work. It seems that the authentication cookie is not shared among the 2 apps on Azure Portal because Azure doesn't allow to access directory location hence it gives error like 'Access Denied' and I don't know how to use Azure Key vault or database to store cookie.
Can you please guide me if I have missed something or have did incorrectly. Your earlier response would be highly appreciated. Let me know if you need any other details.
Regards,
Kiran Shah

Related

ASP.NET 6 WebAPI Authentication with SSO

I have an ASP.NET 6.0 Web API project. I would like to add authentication and authorization to it, but it must use SSO via Azure.
We already have a SPA application that does this, it uses the Angular MSAL library to redirect the user to an SSO Login page, then returns to the SPA with an access token. The access token is then added to the header of each request to the Web API, which uses it to enforce authentication.
Now we want to share our web API with other teams within our organization, and we would like to have that login process just be another API call, rather than a web page.
Conceptually, a client would hit the /login endpoint of our API, passing in a userID and password. The web API would then get an access token from Azure, then return it as the payload of the login request. It's then up to the client to add that token to subsequent request headers.
I have done this with regular ASP.NET Identity, where all of the user and role data is stored in a SQL database, but since our organization uses SSO via Azure Active Directory, we would rather use that.
I have researched this topic online, and so far all of the examples I have seen use a separate SPA, just like we already have. But as this is a web api, not a front-end, we need to have an API method that does this instead.
Is this even possible? I know Microsoft would rather not have user credentials flow through our own web server, where a dishonest programmer might store them for later misuse. I understand that. But I'm not sure there's a way around this.
Thanks.
I believe you are looking for the Resource Owner Password (ROP) flow. You can use IdentityModel.OidcClient to implement it.
Sample code:
public class Program
{
static async Task Main()
{
// call this in your /login endpoint and return the access token to the client
var response = await RequestTokenAsync("bob", "bob");
if (!response.IsError)
{
var accessToken = response.AccessToken;
Console.WriteLine(accessToken);
}
}
static async Task<TokenResponse> RequestTokenAsync(string userName, string password)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Constants.Authority);
if (disco.IsError) throw new Exception(disco.Error);
var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "roclient",
ClientSecret = "secret",
UserName = userName,
Password = password,
Scope = "resource1.scope1 resource2.scope1",
Parameters =
{
{ "acr_values", "tenant:custom_account_store1 foo bar quux" }
}
});
if (response.IsError) throw new Exception(response.Error);
return response;
}
}
Sample taken from IdentityServer4 repository where you can find more ROP flow client examples.
I would recommend that you don't go with this implementation and instead have all clients obtain their access tokens directly from Azure AD like you did with your Angular SPA.

SustainSys library is not including email claim from Azure AD?

I am using IdentityServer3 for authentication and i have also configured Azure AD as external provider in IdentityServer3. Since Azure AD is using SAML2 protocol i am using SustainSys library to configure Azure AD.
In Azure AD i have configure the payload as below. Notice the email is included in the payload
However, after user is authenticated in Azure AD, and request comes back to IdentityServer, SustainSys library is not including email claim.
Is there any configuration i need to do in SustainSys library to include email claim? In IdentityServer3 I am loading Azure AD provider using code
private void AddSAML2Idp(IAppBuilder app, SAML2Provider provider, string signInAsType)
{
var authenticationOptions = new Saml2AuthenticationOptions(false)
{
SPOptions = new SPOptions
{
EntityId = new EntityId(provider.AuduenceURI), // Auduence URI
MinIncomingSigningAlgorithm = provider.MinIncomingSigningAlgorithm,
ModulePath = string.Format("/{0}", provider.Idp)
},
SignInAsAuthenticationType = signInAsType,
AuthenticationType = provider.Idp,
Caption = provider.Caption
};
UseIdSrv3LogoutOnFederatedLogout(app, authenticationOptions);
authenticationOptions.SPOptions.ServiceCertificates.Add(LoadCertificateFromWindwosStore(ApplicationConfig.Saml2SigningCertificateSubjectName));
var identityProvider = new IdentityProvider(new EntityId(provider.EntityID), authenticationOptions.SPOptions)
{
MetadataLocation = provider.MetadataLocation,
LoadMetadata = true
};
authenticationOptions.IdentityProviders.Add(identityProvider);
app.UseSaml2Authentication(authenticationOptions);
}

Azure One Drive access using Username/password auth (native app)

Business Requirement
OneDrive to have a user level folder created and shared to specific members of team.
Owning user to create folders for project and assign exclusive permissions to certain members (remove access to non-project members)
Technical Environment
One Drive access via Graph API
ADAL Auth
Web Job hosted as NativeJob in Azure App and granted required OneDrive access.
App registered in mine (not the account used to access OneDrive)
Current Effort
Since web-api & web apps cannot use username/password auth, required to access a specific user's drive, resorted to WebJob (not sure if it complies as native app).
WebJob tries to auth using the drive owner's credential but fails with message (AADSTS65001: The user or administrator has not consented to use the application with ID '<appId>' named 'OneDriveFileSystem'. Send an interactive authorization request for this user and resource.)
Code
Auth Request
UserCredential uc = new UserPasswordCredential(userName, password);
AuthenticationResult result = null;
try
{
var authContext = new AuthenticationContext(authority);
result = authContext.AcquireTokenAsync(resource, clientId, uc).Result;
}
catch (Exception ee)
{
return string.Empty;
}
return result.AccessToken;
OneDriveAccess
string accessToken = GetAuthToken();
if (!string.IsNullOrEmpty(accessToken))
{
HttpClient httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
string payload = "{ \"name\": \"" + projectRequest.ProjectName + "\" , \"folder\": { } }";
HttpContent content = new StringContent(payload,
System.Text.Encoding.UTF8,
"application/json");
Uri uri = new Uri(resource + "/v1.0" + RootFolderItemId + RootFolderItemId + "/children");
HttpResponseMessage response = httpClient.PostAsync(uri, content).Result;
if (response.IsSuccessStatusCode)
{
var responseContent = response.Content;
}
}
(results in 404, however same url works in Graph Explorer)
Question
Is this design correct or am I missing some critical element?
Do I need to register the app in the account which is used for OneDrive access or can it be allowed to register in any user's account as long as the keys are correct?
If I update the code to use my credential, it asks for multi-factor auth (enabled by admin), but since this is native, this cannot be provided. The drive owner account was created as a service one, so MFA is not applicable to that.
Any other code/configuration required can be provided
Update 1
Using the "me" based endpoint (to default to self's folder) results in a 403 Forbidden, even though I am sending a valid access token. Have granted all permissions for file access (see attached image). The Grant Permission is successful for all demanded permissions.

How to do role based authorization in Azure with Asp.net webapi backend and front end as Angular

How I can do Azure role based authentication in webapi backend. I see tons of example for Asp.net MVC but none for webapi with front end as Angular. I have hosted my website developed in angular with backend as webapi and hosted it on Azure and enabled the Authentication/authorization as Azure Active directory in Azure website. Also I have done the setup for AD application for roles in its manifest files. After I hit website, I have to authenticate and then after authentication it redirects to my website . During my website load it calls my webapi and the Authentication token with claims is passed to my webapi. In webapi I am checking for InRole or [Authorize(Roles = "admin")] and based on that I am allowing access. However in token roles do not flow by default . So I query graph api to get roles by extracting the information from token and pass it to graph api to get roles. I am doing query in Owin Startup class to fetch role, But I am not able to get success. Is there anything wrong with the code, Please help . The code is copied below. Also I could have used ADAL JS library in Angular to authenticate with AD but I don't need to do so as I have enabled authentication in Azure website using the option "Authentication/Authorization". The layer of Authentication in website will do all token validation and forward it to webapi.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IdentityModel.Tokens;
using System.Linq;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.ActiveDirectory;
using Owin;
using System.Security.Claims;
using System.Net;
using System.Web;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Owin.Security.OAuth;
using Microsoft.Azure.ActiveDirectory.GraphClient;
using System.Threading.Tasks;
namespace OneMap.Web
{
public partial class Startup
{
// Apply bearer token authentication middleware to Owin IAppBuilder interface.
private void ConfigureAuth(IAppBuilder app)
{
var tenantId = Common.Configuration.GetConfigurationSetting("ida:Tenant");
// ADAL authentication context for our Azure AD tenant.
var authenticationContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(
$"https://login.windows.net/{tenantId}", true, TokenCache.DefaultShared);
// Secret key that can be generated in the Azure portal to enable authentication of a
// specific application (in this case our Web API) against an Azure AD tenant.
var applicationKey = Common.Configuration.GetConfigurationSetting("ida:Password");
// Root URL for Azure AD Graph API.
var azureGraphApiUrl = "https://graph.windows.net";
var graphApiServiceRootUrl = new Uri(new Uri(azureGraphApiUrl), tenantId);
var clientId = Common.Configuration.GetConfigurationSetting("ida:ClientId");
// Add bearer token authentication middleware.
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
// The id of the client application that must be registered in Azure AD.
TokenValidationParameters = new TokenValidationParameters { ValidAudience = clientId },
// Our Azure AD tenant (e.g.: contoso.onmicrosoft.com).
Tenant = tenantId,
Provider = new OAuthBearerAuthenticationProvider
{
// This is where the magic happens. In this handler we can perform additional
// validations against the authenticated principal or modify the principal.
OnValidateIdentity = async context =>
{
try
{
// Retrieve user JWT token from request.
var authorizationHeader = context.Request.Headers["Authorization"];
var userJwtToken = authorizationHeader.Substring("Bearer ".Length).Trim();
// Get current user identity from authentication ticket.
var authenticationTicket = context.Ticket;
var identity = authenticationTicket.Identity;
// Credential representing the current user. We need this to request a token
// that allows our application access to the Azure Graph API.
var userUpnClaim = identity.FindFirst(ClaimTypes.Upn);
var userName = userUpnClaim == null
? identity.FindFirst(ClaimTypes.Email).Value
: userUpnClaim.Value;
var userAssertion = new UserAssertion(
userJwtToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
// Credential representing our client application in Azure AD.
var clientCredential = new ClientCredential(clientId, applicationKey);
// Get a token on behalf of the current user that lets Azure AD Graph API access
// our Azure AD tenant.
var authenticationResult = await authenticationContext.AcquireTokenAsync(
azureGraphApiUrl, clientCredential, userAssertion).ConfigureAwait(false);
// Create Graph API client and give it the acquired token.
var activeDirectoryClient = new ActiveDirectoryClient(
graphApiServiceRootUrl, () => Task.FromResult(authenticationResult.AccessToken));
// Get current user groups.
var pagedUserGroups =
await activeDirectoryClient.Me.MemberOf.ExecuteAsync().ConfigureAwait(false);
do
{
// Collect groups and add them as role claims to our current principal.
var directoryObjects = pagedUserGroups.CurrentPage.ToList();
foreach (var directoryObject in directoryObjects)
{
var group = directoryObject as Group;
if (group != null)
{
// Add ObjectId of group to current identity as role claim.
identity.AddClaim(new Claim(identity.RoleClaimType, group.ObjectId));
}
}
pagedUserGroups = await pagedUserGroups.GetNextPageAsync().ConfigureAwait(false);
} while (pagedUserGroups != null);
}
catch (Exception e)
{
throw;
}
}
}
});
}
}
}
AFAIK, the roles claim only issued in the id_token. After you change the manifest of app(refer here), you can using the SPA get the id_token and access the web API according the roles via the id_token.
And there is no need to using the Authentication/authorization since you have protect the web API using the Microsoft.Owin.Security.ActiveDirectory OWIN middleware. To authenticate the SPA you can refer this code sample.

Identity Server 3 AzureAd Claims not returning

I have a working Identity Server application, and I'm setting it up to work with Azure AD. I've got my Azure Ad App registration and I can authenticate with it properly.
Looking at this and trying to do something similar to store the 3rd party user IDs associated with a user, but I'm not getting the sub or nameIdentifier claims back from AAD.
Do I need to request these from AzureAD somehow? - Their docs seem to be to be saying that the "sub" claim is (or at least can be) returned: https://learn.microsoft.com/en-us/azure/active-directory/develop/active-directory-token-and-claims#_subject. This article seems to say that sub isn't returned, but it's for multitenant applications, so I'm not too sure if that's relevant.
I'm sure I'm missing something simple, but can't find anything relevant on Google.
Cheers,
Alex
The article Work with claims-based identities is too old and this Azure AD token reference article should be right about the token claims in the token issued by Azure AD.
Based on the test, I could get the sub claim from Azure AD and it also issued by the IdentityServer3 like figure below:
Here is the code I configed for the IdentityServer3 for your reference:
var webApp = WebApp.Start("https://localhost:44333", app =>
{
app.UseIdentityServer(new IdentityServerOptions
{
SiteName = "NDC Demo",
SigningCertificate = cert,
Factory = factory,
AuthenticationOptions = new AuthenticationOptions
{
IdentityProviders = ConfigureAdditionalIdentityProviders,
EnableAutoCallbackForFederatedSignout = true
}
});
});
public static void ConfigureAdditionalIdentityProviders(IAppBuilder app, string signInAsType)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "aad",
Caption = "Azure AD",
SignInAsAuthenticationType = signInAsType,
Authority = "https://login.microsoftonline.com/{tenantId}",
ClientId = "{clientIdFromAzurePortal}",
RedirectUri = "{redirectUri}",
});
}
If you still have the problem, would you mind sharing the request to Azure AD which you can capture it using Fiddler.

Resources