In my .NET MVC project, if a user login in the system, he will get his email and system will send a welcome email later.
My problem is that I don't want every user to login and go through authorization process to get his personal information, such as email, username.
With above condition in mind, after research I found out Microsoft graph can help me with my situation. However I'm not sure where to dig in.
Can anyone give help me how to do this?
According to this reference we can get an Access Token from some background services or daemons.
Based on my test, we can try the following steps:
First, we should get administrator consent:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Scope = "openid profile",
ResponseType = "id_token",
TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, NameClaimType = "name" },
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = this.OnAuthenticationFailedAsync,
SecurityTokenValidated = this.OnSecurityTokenValidatedAsync
}
});
ConfidentialClientApplication daemonClient = new ConfidentialClientApplication(Startup.clientId, string.Format(AuthorityFormat, tenantId), Startup.redirectUri,
new ClientCredential(Startup.clientSecret), null, appTokenCache.GetMsalCacheInstance());
AuthenticationResult authResult = await daemonClient.AcquireTokenForClientAsync(new[] { MSGraphScope });
Second, we can get the user by the email from the url: https://graph.microsoft.com/v1.0/users/{email address}. For example,https://graph.microsoft.com/v1.0/users/xxx.outlook.com
Then, we can use the API like this: POST https://graph.microsoft.com/v1.0/{user id | userPrincipalName}/sendMail, For this, we can refer to Send mail
For more details, we can download the simple from v2.0 daemon sample on GitHub.
Related
I`m using from KeyCloak for SSO (Single Sign On). And I have MVC.NET project to use from it.
So I have this for sent request:
new OpenIdConnectAuthenticationOptions
{
ClientId = client_id,
Authority = issuer_url,
RedirectUri = redirect_uri,
PostLogoutRedirectUri = redirect_uri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
ClientSecret = secret,
SignInAsAuthenticationType = CookieAuthenticationDefaults.AuthenticationType,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed
},
});
=> And I get code OnAuthorizationCodeReceived():
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification n)
{
var client = new HttpClient();
var tokenResponse = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
{
Code = n.Code,
Address = token_endpoint_url,
ClientId = client_id,
ClientSecret = secret,
RedirectUri = redirect_uri,
});
if (tokenResponse.IsError)
throw new Exception(tokenResponse.Error);
var response = await client.GetUserInfoAsync(new UserInfoRequest
{
Token = tokenResponse.AccessToken,
Address = userinfo_endpoint,
});
if (response.IsError)
throw new Exception(response.Error);
n.AuthenticationTicket.Identity.AddClaims(response.Claims);
}
When I use ResponseType only code, the n.AuthenticationTicketis NULL. and I must set response type as codeIdtoken to get n.AuthenticationTicket and I fill claims. Also I have to enable Hybrid Flow on KeyCloak.
I don`t want to change my code on changing flows (Standard Flow Enabled or Implicit Flow Enabled).
Is it possible that I get n.AuthenticationTicket on ResponseType.Code?
Note: I dont have any problem on get n.codealsotokenResponse.AccessToken` but my problem is only on n.AuthenticationTicket.
If it is not possible, what is the standard solution for filling claims?
if you just use ResponseType code, you will get the access token or authorization code, but they are not related to the AuthenticationTicket.
The AuthenticationTicket is created based on the ID-token, because that contains the details about the user. So you need to use code id_token to get it to work.
For modern application and to follow best practices, you should only aim to use the authorization code flow or the client credentials flow.
Some parts of the standards are no longer considered secure and if you follow OAuth 2.1 then the two flows above are the ones that are recommended.
OAuth 2.1 is a summary of OAuth 2.0 +/- all the best practices that has been established since 2.0 was released.
For the example, the Implicit grant (response_type=token) is omitted from this specification. You can still use it, but it is no longer considered best practice.
It states here
that going forward, we must prefer the auth code flow method since "With the plans for third party cookies to be removed from browsers, the implicit grant flow is no longer a suitable authentication method."
I have set up an asp.net mvc 4 web application according to the sample app here. It works when I set up my app registration in AD B2C directory for Access Tokens (used for implicit grant flows). If I switch to "ID Tokens (for implicit and hybrid flows)" as the documentation recommends, I get error that my application is not setup for it.
As I understand from the documentation, I would have to specifiy separate endpoins for /authorize and/token to fetch a token after authorization. I am not sure from looking at the sample though how exactly I can do this. Below is the ConfigureAuth method as you can see in the sample code on github link provided:
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(WellKnownMetadata, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {ReadTasksScope} {WriteTasksScope}",
// ASP.NET web host compatible cookie manager
CookieManager = new SystemWebCookieManager()
}
);
}
Background
So we have an app service that authenticates from Azure AD in another tenancy using OpenIdConnect.
Login works on a dev instance of IIS, and it works on our test app service. We saw the issue on test, and it vanished and didn't return during the entire testing phase of the project.
Now we've deployed to production, and we're seeing the issue again.
The Issue
What we're seeing is that everything will work fine for some time, and then after several hours, the issue will emerge again.
We have a work around fix to restore service - that is to enable and then disable app service authentication in the azure control panel. The opposite works too - to disable and then enable will restore service.
The Code
public void ConfigureAuth(IAppBuilder app)
{
//Azure AD Configuration
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//sets client ID, authority, and RedirectUri as obtained from web config
ClientId = clientId,
ClientSecret = appKey,
Authority = authority,
RedirectUri = redirectUrl,
CallbackPath = new PathString("/"), //use this line for production and test
//page that users are redirected to on logout
PostLogoutRedirectUri = redirectUrl,
//scope - the claims that the app will make
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
//setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
//SaveSigninToken = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
}
}
);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
var code = context.Code;
ClientCredential cred = new ClientCredential(clientId, appKey);
string userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
//this token cache is stateful, we're going to populate it here, but we'll store it elsewhere in-case the user ends up accessing a different instance
AuthenticationContext authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));
// If you create the redirectUri this way, it will contain a trailing slash.
// Make sure you've registered the same exact Uri in the Azure Portal (including the slash).
Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, cred, "https://graph.windows.net");
//populate the persistent token cache
testdb2Entities5 db = new testdb2Entities5();
PersistentTokenCache tc = await db.PersistentTokenCaches.FindAsync(userObjectId);
//if null, populate a new item
if (tc == null)
{
tc = new PersistentTokenCache();
tc.object_id = userObjectId;
tc.token = code;
db.PersistentTokenCaches.Add(tc);
await db.SaveChangesAsync();
}
else
{
tc.token = code;
await db.SaveChangesAsync();
}
}
//authentication failed notifications
private Task OnAuthenticationFailed(AuthenticationFailedNotification<Microsoft.IdentityModel.Protocols
.OpenIdConnect.OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
The Question
So, based on whatever enabling and disabling app-service authentication does, it's clearly fixing thing temporarily. So I'm thinking this is a cookie related problem - as that'd be the only thing transferring state between sessions. What on earth could be the issue here? And what steps do I need to take to diagnose and resolve the issue?
So far, it seems like it was a problem with a known bug in Katana where the Katana cookie manager and the ASP .NET cookie manager clash and overwrite each other's cookies.
Here are some troubleshoots you could refer to:
1.Setting
app.UseCookieAuthentication(new CookieAuthenticationOptions {CookieSecure == CookieSecureOption.Always}). This means that cookie could leak along with your auth.
2.Add SystemWebCookieManager in UseCookieAuthentication which is in the Microsoft.Owin.Host.SystemWeb Nuget package. Please refer to this thread.
3.Split cookie. Someone noticed that it was issue if Cookie characters are more than browsers limit (> 4096). So to overcome that issue, in set-cookie with around 4000 characters with each and when needed combine all cookie together to get original value.
For more details about how to add sign-in with Microsoft to an ASP.NET web app, please refer to this article.
Update:
Fix with install Kentor.OwinCookieSaver nuget package and add app.UseKentorOwinCookieSaver(); before app.UseCookieAuthentication(new CookieAuthenticationOptions());
Background
We developed an application in 2016 that authenticated using WS-Federation, to grab claims from the on-premises AD. The direction of the IT strategy has changed, and is moving toward Azure AD (currently hosting a hybrid environment).
We're in the process of migrating the authentication from WS-Fed, to AAD, using OpenIDConnect. Getting the user signed in and authenticated with the new method was surprisingly straightforward - do the config properly, and issue the authenticate challenge, and Robert is your mother's brother.
The Problem
Please correct me if I'm getting my terminology wrong here; we need to grab some attributes from Active Directory that aren't accessible (as far as I can tell) via the default JWT. So, we need to pass the JWT to the Graph API, via HTTP, to get the attributes we want from active directory.
I know that a properly formatted and authenticated request can pull the necessary data, because I've managed to see it using the graph explorer (the AAD one, not the Microsoft Graph one).
The Question
If my understanding above is correct, how do I pull the JWT from the HttpContext in ASP.Net? If I've grasped all this lower level HTTP stuff correctly, I need to include the JWT in the request header for the Graph API request, and I should get the JSON document I need as a response.
(Edit, for the benefit of future readers: You actually need to acquire a new token for the specific service you're trying to access, in this case Azure AD. You can do this using the on-behalf-of flow, or using the as-an-application flow).
Request.Headers["IdToken"] is returning null, so I'm wondering what's going wrong here.
The Code
Here's our Authentication config that runs on server startup:
public void Configuration(IAppBuilder app)
{
AntiForgeryConfig.SuppressIdentityHeuristicChecks = true;
//ConfigureAuth(app); //Old WsFed Auth Code
//start the quartz task scheduler
//RCHTaskScheduler.Start();
//Azure AD Configuration
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//sets client ID, authority, and RedirectUri as obtained from web config
ClientId = clientId,
ClientSecret = appKey,
Authority = authority,
RedirectUri = redirectUrl,
//page that users are redirected to on logout
PostLogoutRedirectUri = redirectUrl,
//scope - the claims that the app will make
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken,
//setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
SaveSigninToken = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
}
}
);
}
Here's my partially complete code for crafting the GraphAPI request:
public static async Task<int> getEmployeeNumber(HttpContextBase context)
{
string token;
int employeeId = -1;
string path = "https://graph.windows.net/<domain>/users/<AAD_USER_ID>?api-version=1.6";
HttpWebRequest request = null;
request = (HttpWebRequest)HttpWebRequest.Create(path);
request.Method = "GET";
request.Headers.Add(context.GetOwinContext().Request.Headers["IdToken"]);
WebResponse response = await request.GetResponseAsync();
throw new NotImplementedException();
}
Okay it took me a few days to work out (and some pointers from Juunas), but this is definitely doable with some slight modifications to the code here. The aforementioned being the OpenId guide from Microsoft.
I would definitely recommend reading up on your specific authentication scenario, and having a look at the relevant samples.
The above will get you in the door, but to get a JWT from the Graph API, (not to be confused with Microsoft Graph), you need to get an authentication code when you authenticate, and store it in a token cache.
You can get a usable token cache out of this sample from Microsoft (MIT License). Now, personally, I find those samples to be overly obfuscated with complicated use-cases, when really they should be outlining the basics, but that's just me. Nevertheless, these are enough to get you close.
Now for some code. Allow me to draw your attention to the 'ResponseType= CodeIdToken'.
public void ConfigureAuth(IAppBuilder app)
{
//Azure AD Configuration
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//sets client ID, authority, and RedirectUri as obtained from web config
ClientId = clientId,
ClientSecret = appKey,
Authority = authority,
RedirectUri = redirectUrl,
//page that users are redirected to on logout
PostLogoutRedirectUri = redirectUrl,
//scope - the claims that the app will make
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.CodeIdToken,
//setup multi-tennant support here, or set ValidateIssuer = true to config for single tennancy
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
//SaveSigninToken = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
}
}
);
}
When the above parameter is supplied, the following code will run when you authenticate:
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
var code = context.Code;
ClientCredential cred = new ClientCredential(clientId, appKey);
string userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
AuthenticationContext authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));
// If you create the redirectUri this way, it will contain a trailing slash.
// Make sure you've registered the same exact Uri in the Azure Portal (including the slash).
Uri uri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path));
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(code, uri, cred, "https://graph.windows.net");
}
This will supply your token cache with a code that you can pass to the Graph API. From here, we can attempt to authenticate with the Graph API.
string path = "https://graph.windows.net/me?api-version=1.6";
string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
string userObjectId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
string resource = "https://graph.windows.net";
AuthenticationResult result = null;
string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
ClientCredential cc = new ClientCredential(ConfigurationManager.AppSettings["ClientId"], ConfigurationManager.AppSettings["ClientSecret"]);
AuthenticationContext auth = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId));
try
{
result = await auth.AcquireTokenSilentAsync(resource,
ConfigurationManager.AppSettings["ClientId"],
new UserIdentifier(userObjectId, UserIdentifierType.UniqueId)).ConfigureAwait(false);
}
catch (AdalSilentTokenAcquisitionException e)
{
result = await auth.AcquireTokenAsync(resource, cc, new UserAssertion(userObjectId));
}
Once you have the authentication token, you can pass it to the Graph API via Http Request (this is the easy part).
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(path);
request.Method = "GET";
request.Headers.Set(HttpRequestHeader.Authorization, "Bearer " + result.AccessToken);
WebResponse response = request.GetResponse();
System.IO.Stream dataStream = response.GetResponseStream();
From here, you have a datastream that you can pass into a stream reader, get the JSON out of, and do whatever you want with. In my case, I'm simply looking for user data that's in the directory, but is not contained in the default claims that come out of Azure AD Authentication. So in my case, the URL I'm calling is
"https://graph.windows.net/me?api-version=1.6"
If you need to do a deeper dive on your directory, I'd recommend playing with the Graph Explorer. That will help you structure your API calls. Now again, I find the Microsoft documentation a little obtuse (go look at the Twilio API if you want to see something slick). But it's actually not that bad once you figure it out.
EDIT: This question has since gotten a 'notable question' badge from Stack Overflow. Please note, this addresses the ADAL implementation for Azure AD Auth in this scenario. You should be using MSAL, as ADAL is now deprecated! It's mostly the same but there are some key differences in the implementation.
I am using the Microsoft.Security.Owin.OpenIdConnect to implement Single Sign-On in my ASP.NET MVC 5 app. This is the code I am using:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions {
ClientId = "id",
ClientSecret = "secret",
MetadataAddress = "https://accounts.google.com/.well-known/openid-configuration",
RedirectUri = "http://localhost:****",
ResponseType = "code id_token",
Scope = "openid email profile",
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie
});
However, I want to get the access token, and if necessary the refresh token, to use for API codes. There is very little documentation for how to do this. Apparently I need to use AcquireTokenByAuthorizationCode, but I can only find this function in an ActiveDirectory assembly, which doesn't seem like it is something that would be used for OpenId. How do I set the options so I can get the access token to use in APIs?
In your ResponseType, put token as well