Passing parameters to Azure Active Directory authentication - asp.net-mvc

I have an ASP.Net MVC Application, Owin, and I'm using Azure Active Directory authentication as well.
I want to pass a parameter when the user is redirected to the Azure AD authentication page. So when the user signs in or signs up I want to pass ProjectId (int) as a parameter.
After the user signs in/up and is redirected to my Application I want to receive the ProjectId I passed as a parameter.
How can I achieve that?
edit: Adding code
// The ACR claim is used to indicate which policy was executed
public const string AcrClaimType = "http://schemas.microsoft.com/claims/authnclassreference";
public const string PolicyKey = "b2cpolicy";
private const string OidcMetadataSuffix = "/.well-known/openid-configuration";
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
OpenIdConnectAuthenticationOptions options = new OpenIdConnectAuthenticationOptions
{
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
UseTokenLifetime = false,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = AuthenticationFailed,
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
SecurityTokenValidated = OnSecurityTokenValidated
},
Scope = "openid",
ResponseType = "id_token",
// The PolicyConfigurationManager takes care of getting the correct Azure AD authentication
// endpoints from the OpenID Connect metadata endpoint. It is included in the PolicyAuthHelpers folder.
ConfigurationManager = new PolicyConfigurationManager(
string.Format(CultureInfo.InvariantCulture, AadInstance, Tenant, "/v2.0", OidcMetadataSuffix),
new[] { SignUpPolicyId, SignInPolicyId, ProfilePolicyId }),
// This piece is optional - it is used for displaying the user's name in the navigation bar.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
}
};
app.UseOpenIdConnectAuthentication(options);
}
private Task OnRedirectToIdentityProvider(
RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var currentPolicy =
notification.OwinContext.Authentication.AuthenticationResponseRevoke.AuthenticationTypes
.FirstOrDefault(x => x.StartsWith("b2c"));
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.Split('?')[0];
notification.ProtocolMessage.Parameters.Add("p", currentPolicy);
}
else
{
**// The value right now for the state is sort of "hijacked" and assigned by Microsoft**
//notification.ProtocolMessage.Parameters["state"] = "OpenIdConnect.AuthenticationProperties=sRt-teBcxsd239viWo...... ";
var currentPolicy = notification.OwinContext.Authentication.AuthenticationResponseChallenge.Properties
.Dictionary[PolicyKey];
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.Split('?')[0];
notification.ProtocolMessage.Parameters.Add("p", currentPolicy);
}
return Task.FromResult(0);
}
private async Task OnSecurityTokenValidated(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
await MyClass.CreatePrincipal(notification.AuthenticationTicket.Identity);
}
private Task AuthenticationFailed(
AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
return Task.FromResult(0);
}

Similar to what Gaurav is suggesting, but adding a few special considerations. Basically, the state is used by the Owin middleware, so while you can inject your own stuff, you need to make sure you revert it back before the Owin middleware tries to use it otherwise you'll get auth errors.
This is effectively what I replied to a very similar question:
Custom parameter with Microsoft.Owin.Security.OpenIdConnect and AzureAD v 2.0 endpoint
In Startup.Auth.cs, when you setup the OpenIdConnectAuthenticationOptions you'd add the following:
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//...
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
MessageReceived = OnMessageReceived
},
});
And use RedirectToIdentityProvider to inject your parameter, something along the lines of:
private static Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var stateQueryString = notification.ProtocolMessage.State.Split('=');
var protectedState = stateQueryString[1];
var state = notification.Options.StateDataFormat.Unprotect(protectedState);
state.Dictionary.Add("mycustomparameter", "myvalue");
notification.ProtocolMessage.State = stateQueryString[0] + "=" + notification.Options.StateDataFormat.Protect(state);
return Task.FromResult(0);
}
And then use MessageReceived to extract it, like so:
private static Task OnMessageReceived(MessageReceivedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
string mycustomparameter;
var protectedState = notification.ProtocolMessage.State.Split('=')[1];
var state = notification.Options.StateDataFormat.Unprotect(protectedState);
state.Dictionary.TryGetValue("mycustomparameter", out mycustomparameter);
return Task.FromResult(0);
}
You'd obviously need to improve/harden this but this should get you going.

Just add context.ProtocolMessage.SetParameter(<ParameterName>, <value>); in RedirectToIdentityProvider
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("prompt", "login");
return Task.FromResult(0);
},
}
};

You could pass the ProjectId parameter as value for State parameter. See the sample code below:
Notifications = new OpenIdConnectAuthenticationNotifications()
{
RedirectToIdentityProvider = context =>
{
redirectUri = string.Format("{0}/", System.Web.HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority));
postLogoutRedirectUri = redirectUri + "sign-out";
context.ProtocolMessage.RedirectUri = redirectUri;
context.ProtocolMessage.PostLogoutRedirectUri = postLogoutRedirectUri;
context.ProtocolMessage.State = "Your Project Id";
return Task.FromResult(0);
},
AuthorizationCodeReceived = context =>
{
var projectId = context.ProtocolMessage.State;//Retrieve the state in AuthorizationCodeReceived event.
return Task.FromResult(0);
}
}
};
UPDATE
Essentially State accepts a string parameter. In our project we needed to provide many values in the state. What we did there is created a pipe delimited string and pass that as state. When we receive the state back, we simply convert that into an array and use appropriate elements. Something like:
var state = "param1|param2|...|paramx";
Other thing you could do is create a state object (a simple class with some properties), serialize it as JSON, convert that in base64 string and pass that encoded string as state after properly url encoding it. When you receive back the state, you could do the reverse process, get state object back and use it's properties values.

Related

Unable to cast object of type 'Microsoft.Graph.CalendarEventsCollectionPage' to type 'System.Collections.Generic.IEnumerable`

My controller code is lke this,
public async Task<IEnumerable<CalendarEvent>> Get()
{
var scopes = new[] { "https://graph.microsoft.com/.default" };
var tenantId = "xxxxx";
var clientId = "xxxxxx";
var clientSecret = "xxxx";
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret);
var graphServiceClient = new GraphServiceClient(clientSecretCredential, scopes);
if (User == null!)
{
var user = await graphServiceClient.Users["xxxxx.com"].Calendar
.Events
.Request()
.Select("subject,body,bodyPreview,organizer,attendees,start,end,location")
.GetAsync();
return (CalendarEvent)user;
}
}
Iam getting an error like
Unable to cast object of type 'Microsoft.Graph.CalendarEventsCollectionPage' to type 'System.Collections.Generic.IEnumerable'
I need query that sholud be given in controller.
It's not exactly clear what you are trying to achieve but you can't convert CalendarEventsCollectionPage to IEnumerable. I am assuming that you want to return all events of specific user.
public async Task<List<Event>> GetEventsOfUser(string userId)
{
var events = new List<Event>();
var eventsPages = _client.Users[userId].Calendar.Events.Request()
.Select("subject,body,bodyPreview,organizer,attendees,start,end,location");
while (eventsPages != null)
{
var current = await eventsPages.GetAsync();
events.AddRange(current.CurrentPage);
eventsPages = current.NextPageRequest;
}
return events;
}
You need to fetch every page with NextPageRequest in order to get all events.

Connect to Office365 via backend service using OAuth2 in NON interactive way (bar initial setup)

I have a background service which reads & sends from a mailbox. It is created in a web ui, but after the schedule is created and mailbox set, it should run automatically, without further user prompt.
I have used the various combinations of the MSAL and both public and confidential clients (either would be acceptable as the server can maintain the client secret.
I have used the EWS client and got that working, but there is a note that the client_credentials flow won't work for IMAP/POP/SMTP.
I have a small console app working, but each time it runs, it needs to login interactively, and so long as I don't restart the application, it will keep authenticating, and I can call the AquireTokenSilently.
The Question
How can I make the MSAL save the tokens/data such that when it next runs, I can authenticate without user interaction again? I can store whatever is needed to make this work when the user authenticates, but I don't know what that should be nor how to reinstate it to make a new request, if the console app is restarted.
The Code
internal async Task<string> Test()
{
PublicClientApplication =
PublicClientApplicationBuilder.Create( "5896de31-e251-460c-9dc2-xxxxxxxxxxxx" )
.WithRedirectUri( "https://login.microsoftonline.com/common/oauth2/nativeclient" )
.WithAuthority( AzureCloudInstance.AzurePublic, ConfigurationManager.AppSettings["tenantId"] )
.Build();
//var scopes = new string[] { "email", "offline_access", "profile", "User.Read", "Mail.Read" };
var scopes = new string[] { "https://outlook.office.com/IMAP.AccessAsUser.All" };
var accounts = await PublicClientApplication.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
AuthenticationResult authResult;
if (firstAccount == null )
{
authResult = await PublicClientApplication.AcquireTokenInteractive( scopes ).ExecuteAsync();
}
else
{
//The firstAccount is null when the console app is run again
authResult = await PublicClientApplication.AcquireTokenSilent( scopes, firstAccount ).ExecuteAsync();
}
if(authResult == null)
{
authResult = await PublicClientApplication.AcquireTokenInteractive( scopes ).ExecuteAsync();
}
MailBee.Global.LicenseKey = "MN120-569E9E8D9E5B9E8D9EC8C4BC83D3-D428"; // (demo licence only)
MailBee.ImapMail.Imap imap = new MailBee.ImapMail.Imap();
var xOAuthkey = MailBee.OAuth2.GetXOAuthKeyStatic( authResult.Account.Username, authResult.AccessToken );
imap.Connect( "imap.outlook.com", 993 );
imap.Login( null, xOAuthkey, AuthenticationMethods.SaslOAuth2, AuthenticationOptions.None, null );
imap.SelectFolder( "INBOX" );
var count = imap.MessageCount.ToString();
return authResult.AccessToken;
}
It feels very much like a step missed, which can store the information to make subsequent requests and I would love a pointer in the right direction please.
When you create your PublicClientApplication, it provides you with the UserTokenCache.
UserTokenCache implements interface ITokenCache, which defines events to subscribe to token cache serialization requests as well as methods to serialize or de-serialize the cache at various formats.
You should create your own TokenCacheBuilder, which can store the tokens in file/memory/database etc.. and then use the events to subscribe to to token cache request.
An example of a FileTokenCacheProvider:
public abstract class MsalTokenCacheProviderBase
{
private Microsoft.Identity.Client.ITokenCache cache;
private bool initialized = false;
public MsalTokenCacheProviderBase()
{
}
public void InitializeCache(Microsoft.Identity.Client.ITokenCache tokenCache)
{
if (initialized)
return;
cache = tokenCache;
cache.SetBeforeAccessAsync(OnBeforeAccessAsync);
cache.SetAfterAccessAsync(OnAfterAccessAsync);
initialized = true;
}
private async Task OnAfterAccessAsync(TokenCacheNotificationArgs args)
{
if (args.HasStateChanged)
{
if (args.HasTokens)
{
await StoreAsync(args.Account.HomeAccountId.Identifier,
args.TokenCache.SerializeMsalV3()).ConfigureAwait(false);
}
else
{
// No token in the cache. we can remove the cache entry
await DeleteAsync<bool>(args.SuggestedCacheKey).ConfigureAwait(false);
}
}
}
private async Task OnBeforeAccessAsync(TokenCacheNotificationArgs args)
{
if (!string.IsNullOrEmpty(args.SuggestedCacheKey))
{
byte[] tokenCacheBytes = await GetAsync<byte[]>(args.SuggestedCacheKey).ConfigureAwait(false);
args.TokenCache.DeserializeMsalV3(tokenCacheBytes, shouldClearExistingCache: true);
}
}
protected virtual Task OnBeforeWriteAsync(TokenCacheNotificationArgs args)
{
return Task.CompletedTask;
}
public abstract Task StoreAsync<T>(string key, T value);
public abstract Task DeleteAsync<T>(string key);
public abstract Task<T> GetAsync<T>(string key);
public abstract Task ClearAsync();
}
And the MsalFileTokenCacheProvider:
public sealed class MsalFileTokenCacheProvider : MsalTokenCacheProviderBase
{
private string basePath;
public MsalFileTokenCacheProvider(string basePath)
{
this.basePath = basePath;
}
public override Task ClearAsync()
{
throw new NotImplementedException();
}
public override Task DeleteAsync<T>(string key)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Key MUST have a value");
}
string path = Path.Combine(basePath, key + ".json");
if (File.Exists(path))
File.Delete(path);
return Task.FromResult(true);
}
public override Task<T> GetAsync<T>(string key)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Key MUST have a value");
}
string path = Path.Combine(basePath, key + ".json");
if (File.Exists(path))
{
T value = JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
return Task.FromResult(value);
}
else
return Task.FromResult(default(T));
}
public override Task StoreAsync<T>(string key, T value)
{
string contents = JsonConvert.SerializeObject(value);
string path = Path.Combine(basePath, key + ".json");
File.WriteAllText(path, contents);
return Task.FromResult(value);
}
}
So based on your code you will have:
PublicClientApplication =
PublicClientApplicationBuilder.Create( "5896de31-e251-460c-9dc2-xxxxxxxxxxxx" )
.WithRedirectUri( "https://login.microsoftonline.com/common/oauth2/nativeclient" )
.WithAuthority( AzureCloudInstance.AzurePublic, ConfigurationManager.AppSettings["tenantId"] )
.Build();
MsalFileTokenCacheProvider cacheProvider = new MsalFileTokenCacheProvider("TokensFolder");
cacheProvider.InitializeCache(PublicClientApplication.UserTokenCache);
//var scopes = new string[] { "email", "offline_access", "profile", "User.Read", "Mail.Read" };
var scopes = new string[] { "https://outlook.office.com/IMAP.AccessAsUser.All" };
// when you call the below code, the PublicClientApplication will use your token cache
//provider in order to get the required Account. You should also use the
//PublicClientApplication.GetAccountAsync(key) which will use the token cache provider for
//the specific account that you want to get the token. If there is an account you could
//just call the AcquireTokenSilent method. The acquireTokenSilent method will take care of the token expiration and will refresh if needed.
//Please bare in mind that in some circumstances the AcquireTokenSilent method will fail and you will have to use the AcquireTokenInteractive method again. //Example of this would be when the user changes password, or has removed the access to your Application via their Account.
var accounts = await PublicClientApplication.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
Please refer to the following documentation from Microsoft.
https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-net-token-cache-serialization

How to access Shared Mail Folder Using Microsoft Graph In .NET Application

I have Generated Microsoft Graph app in ASP.NET MVC platform, that I have downloaded from Microsoft Graph site. I need to access the shared mail folder not sure exactly how can I get that?? In the following code I can access my mailFolder but not shared mailfolder!
public static async Task<IEnumerable<MailFolder>> GetMailFolderAsync()
{
var graphClient = GetAuthenticatedClient();
var mailFolder = await graphClient.Me.MailFolders.Request().GetAsync();
var sharedMailFolder = await graphClient.Users.Request().GetAsync();
return mailFolder;
}
Also, I want to know in above code where I can pass the parameter to access next page or all pages??
private static GraphServiceClient GetAuthenticatedClient()
{
return new GraphServiceClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string signedInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
SessionTokenStore tokenStore = new SessionTokenStore(signedInUserId,
new HttpContextWrapper(HttpContext.Current));
var idClient = new ConfidentialClientApplication(
appId, redirectUri, new ClientCredential(appSecret),
tokenStore.GetMsalCacheInstance(), null);
var accounts = await idClient.GetAccountsAsync();
var result = await idClient.AcquireTokenSilentAsync(
graphScopes.Split(' '), accounts.FirstOrDefault());
requestMessage.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", result.AccessToken);
}));
I think it is not possible to access shared folders I am investigating as well. In regards to the question of getting pages, as soon as you get the first request
public static async Task<IEnumerable<MailFolder>> GetMailFolderAsync()
{
var graphClient = GetAuthenticatedClient();
var mailFolder = await graphClient.Me.MailFolders.Request().GetAsync();
var sharedMailFolder = await graphClient.Users.Request().GetAsync();
return mailFolder;
}
then you can review for example, mailFolder.NextPageRequest, if it is not null then you can request it by doing mailFolder.NextPageRequest.GetAsync() and you can use it as a loop conditional
while(mailfoldersCollection != null) {
// Do your stuff with items within for(var folder in mailfoldersCollection) {}
// when read all items in CurrentPage then
if (mailFolder.NextPageRequest != null) {
mailfoldersCollection = await mailFolder.NextPageRequest.GetAsync();
}
hope it works for you!

Missing Access Token on IsAuthorized at Attribute

I
have been following the DotnetOPenAuth example of the oAuth authorization.
on the OAuth2AuthorizeAttribute i am always getting "Missing Access Token."
I have checked again and again i am passing the access token via the header and its been set nicely within the request object.
What could be the reason for that?
Here is the code
protected virtual bool IsAuthorized(HttpActionContext actionContext, out IPrincipal user)
{
var signingKey = ApplicationSettings.SigningKey(ApplicationSettings.KeyType.Public);
var resourceKey = ApplicationSettings.ResoureKey(ApplicationSettings.KeyType.Private);
using (var signing = signingKey)
using (var resource = resourceKey)
{
base.OnAuthorization(actionContext);
// TODO FIXME dnoa doesn't support HttpRequestMessage - manually creating HttpRequestMessageProperty until they do
var request = new HttpRequestMessageProperty();
if (actionContext.Request.Headers.Authorization != null)
{
request.Headers[HttpRequestHeader.Authorization] =
actionContext.Request.Headers.Authorization.ToString();
}
else
{
request.Headers[HttpRequestHeader.Authorization] = null;
}
var requestUri = actionContext.Request.RequestUri;
var resourceServer = new ResourceServer(new StandardAccessTokenAnalyzer(signing, resource));
try
{
user = resourceServer.GetPrincipal(request, requestUri, _oauth2Scopes);
return true;
}
catch (ProtocolFaultResponseException x)
{
user = null;
return false;
}
}
}
I found the problem.
In the Authorization header need to have the text Bearer before the access token for example
Bearer gAAAALfeAiFpUFOY8bJggyQ

Windows 8 Httpclient basic authentcation

I am new to this forum.
I am trying to do Basic authentication using Httclient for my Windows app.
var handler2 = new HttpClientHandler
{
Credentials = new NetworkCredential(username, password)
};
var httpClient2 = new HttpClient(handler2);
httpClient2.DefaultRequestHeaders.Add("user-Agent", "authentication.cs");
var response2 = httpClient.GetAsync(uri);
I have 2 questions:
I need to add header content type and user-agent. Dont know how to add them. Could someone help me out.
In response i am getting null values. Any idea why?
Regards,
TM
You can add the user agent header by doing
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("authentication.cs"));
You can't add Content-Type to the default request headers because you can only set Content-Type when you are sending some Content using a PUT or a POST.
I'm guessing you want to set the Accept header like this:
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
Update: Without my own account, this is as far as I can go.
public sealed partial class MainPage : Page
{
private readonly HttpClient _httpClient = new HttpClient();
public MainPage()
{
this.InitializeComponent();
InitHttpClient();
}
private void InitHttpClient() {
var username = "youremail#somewhere.com";
var password = "yourharvestpassword";
String authparam = System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(username + ":" + password));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authparam);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyHarvestClient", "1.0"));
}
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached. The Parameter
/// property is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e) {
_httpClient.GetAsync("https://yoursubdomain.harvestapp.com/projects")
.ContinueWith(t => HandleResponse(t.Result));
}
private void HandleResponse(HttpResponseMessage response) {
response.EnsureSuccessStatusCode();
var contentString = response.Content.ReadAsStringAsync().Result;
var contentXML = XDocument.Parse(contentString);
}
}

Resources