Getting shared calendar events with Microsoft.Graph - asp.net-core

I'm trying to recreate the following example: https://learn.microsoft.com/en-us/graph/outlook-get-shared-events-calendars
I'm using ASP.NET Core 3.1, Azure AD, and the Microsoft.Graph API. I was able to retrieve events from my personal calendar, as well as the name, ID, owner etc. from the shared calendars. The events from the shared calendars, however, refuse to show. I couldn't find any examples, Microsoft's or otherwise.
Here's the code I'm using:
[Authorize]
[AuthorizeForScopes(ScopeKeySection = "MicrosoftGraph:Scopes")]
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly GraphServiceClient graphServiceClient;
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler;
private string[] _graphScopes;
public HomeController(ILogger<HomeController> logger, GraphServiceClient gsc,
MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler
)
{
_logger = logger;
graphServiceClient = gsc;
this._consentHandler = consentHandler;
_graphScopes = Startup.ConfigurationInstance.GetSection("MicrosoftGraph")["Scopes"].Split(" ");
}
[AuthorizeForScopes(ScopeKeySection = "MicrosoftGraph:Scopes")]
public IActionResult Index()
{
User currentUser = null;
try
{
currentUser = graphServiceClient.Me.Request().GetAsync().Result;
}
// Catch CAE exception from Graph SDK
catch (ServiceException svcex) when (svcex.Message.Contains("Continuous access evaluation resulted in claims challenge"))
{
try
{
Console.WriteLine($"{svcex}");
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
_consentHandler.ChallengeUser(_graphScopes, claimChallenge);
return new EmptyResult();
}
catch (Exception ex2)
{
_consentHandler.HandleException(ex2);
}
}
var viewOptions = new List<QueryOption>
{
new QueryOption("startDateTime", DateTime.Today.ToString("o")),
new QueryOption("endDateTime", DateTime.Today.AddDays(20).ToString("o"))
};
var sha = graphServiceClient.Me.Calendars
.Request()
.Select(e => e.Id)
.GetAsync().Result.ToList();
var x = new List<Event>();
foreach(var row in sha)
{
var id = row.Id;
var data = graphServiceClient.Me.Calendars[id].Request(viewOptions)
.Select(e => e.Events).GetAsync().Result;
ICalendarEventsCollectionPage d1 = data != null? data.Events: null;
if (d1 != null)
{
x.AddRange(d1.ToList());
}
}
}
}
The above retrieves null events.
I tried to access the calendar directly from another user, as per another one of Microsoft's examples:
var x = graphServiceClient.Users["<email>"]
.CalendarView
.Request(viewOptions)
.Select(e => new
{
e.Subject,
e.Organizer,
e.Start,
e.End
}).GetAsync().Result;
The only thing I get from this code is 'access denied'.
My appsettings.json with the scopes set in the Azure AD tenant:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "<domain>",
"TenantId": "<id>",
"ClientId": "<id>",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath": "/signout-callback-oidc",
"ClientSecret": "<secret>"
},
"AllowedHosts": "*",
"MicrosoftGraph": {
"Scopes": "User.Read MailboxSettings.ReadWrite Calendars.ReadWrite.Shared",
"BaseUrl": "https://graph.microsoft.com/v1.0"
}
Any help with this would be greatly appreciated.

Your code and configuration don't seem to be the problem. Please check whether the Permissions to allow access is enabled in Outlook's calendar.
If the problem persists, here is a detailed documentation to guide you through the configuration step by step. You can refer to this documentation to see if some steps are missing.
Hop this can help you.

Related

Application Insights RequestTelemetry not showing up in Requests after exception

I have spent a while trying to RequestTelemetry to work. It did when I was first playing around with it, but then oddly just stopped working whenever an exception is thrown. I have read documentation using Application Insights for custom events and metrics as well as Custom Operations Tracking and tried to add all of the best practices to see if I could get the result to show up again. I'm using .NET Core 3.1 and Microsoft.ApplicationInsights.AspNetCore 2.14.0.
Setup for the Webapp looks like this in Startup.cs
services.AddApplicationInsightsTelemetry(new ApplicationInsightsServiceOptions {
EnableAdaptiveSampling = false
});
I have the telemetry inside of a Controller Post Action. I realize that Application Insights is already tracking it the post action, but I wanted to see if I could track the inner method. This is the code in my controller:
public MyController(IMyService myService, TelemetryClient telemetryClient, ILogger<MyController> logger) {
_myService = myService;
_telemetryClient = telemetryClient;
_logger = logger;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> PostAsync([FromBody] MyModel model) {
using var scope = _logger.BeginScope(new Dictionary<string, object> {
{ $"{nameof(PostAsync)}.Scope", Guid.NewGuid() },
{ nameof(model.Name), model.Name }
});
model.AuthenticatedUserId = User.GetUserIdFromClaims();
var requestTelemetry = new RequestTelemetry { Name = nameof( _myService.MyFunctionAsync) };
var operation = _telemetryClient.StartOperation(requestTelemetry);
operation.Telemetry.Properties.Add("User", model.AuthenticatedUserId);
try {
await _myService.MyFunctionAsync(model).ConfigureAwait(false); // <-- throws exception
operation.Telemetry.Success = true;
return NoContent();
} catch (Exception e) {
operation.Telemetry.Success = false;
throw;
} finally {
_telemetryClient.StopOperation(operation);
}
}
I can see in the Visual Studio console output that the code executes, as I get the following log, but it never shows up in the Application Insights Requests.
Application Insights Telemetry: {
"name": "AppRequests",
"time": "2020-06-21T14:29:08.7469588Z",
"iKey": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"tags": {
"ai.application.ver": "1.0.0.0",
"ai.cloud.roleInstance": "DESKTOP-K74PNCU",
"ai.operation.id": "0443259d660125498cf28f8f7a275dab",
"ai.operation.parentId": "1dea6f9b27220c4c",
"ai.operation.name": "POST EventEmitter/Post",
"ai.location.ip": "::1",
"ai.internal.sdkVersion": "dotnetc:2.14.0-17971",
"ai.internal.nodeName": "DESKTOP-K74PNCU"
},
"data": {
"baseType": "RequestData",
"baseData": {
"ver": 2,
"id": "2b7900eedfb7c34d",
"name": "MyFunctionAsync",
"duration": "00:00:00.3766937",
"success": false,
"properties": {
"DeveloperMode": "true",
"User": "pobl-dev",
"_MS.ProcessedByMetricExtractors": "(Name:'Requests', Ver:'1.1')",
"AspNetCoreEnvironment": "Development"
}
}
}
}
There is a simple solution, but I'm not sure of why it's necessary, due to either a lack in documentation or a bug. I found once a responseCode was provided everything works fine. There is a default responseCode of 200 which shows up on a successful call. Once I set the value on a failure everything worked fine.
public MyController(IMyService myService, TelemetryClient telemetryClient, ILogger<MyController> logger) {
_myService = myService;
_telemetryClient = telemetryClient;
_logger = logger;
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> PostAsync([FromBody] MyModel model) {
using var scope = _logger.BeginScope(new Dictionary<string, object> {
{ $"{nameof(PostAsync)}.Scope", Guid.NewGuid() },
{ nameof(model.Name), model.Name }
});
model.AuthenticatedUserId = User.GetUserIdFromClaims();
var requestTelemetry = new RequestTelemetry { Name = nameof( _myService.MyFunctionAsync) };
var operation = _telemetryClient.StartOperation(requestTelemetry);
operation.Telemetry.Properties.Add("User", model.AuthenticatedUserId);
try {
await _myService.MyFunctionAsync(model).ConfigureAwait(false); // <-- throws exception
operation.Telemetry.Success = true;
operation.Telemetry.ResponseCode = "Roses";
return NoContent();
} catch (Exception e) {
operation.Telemetry.Success = false;
operation.Telemetry.ResponseCode = "Funky"; // <-- seems to be required on a failure
throw;
} finally {
_telemetryClient.StopOperation(operation);
}
}
This is to add some context to the accepted answer if you're curious:
Here's the source code for RequestTelemetry
When it prepares the data to send to Azure servers it explicitly elects NOT to set a default response code unless success == true in which case the default is 200.
// Required fields
if (!this.Success.HasValue)
{
this.Success = true;
}
if (string.IsNullOrEmpty(this.ResponseCode))
{
this.ResponseCode = this.Success.Value ? "200" : string.Empty;
}
If you run a simple Kusto query on the logs:
union requests
| where timestamp > ago(1hr)
| where customDimensions["CustomOperationCategory"] in ("Identity")
| take 100
You'll only see unsuccessful results where you did set a status code:
I don't know if something ever changed, but Microsoft's examples sometimes do the same.

IdentityServer 4 - Custom IExtensionGrantValidator always return invalid_grant

My app requirements is to authenticate using client credentials AND another code (hash).
I followed this link to create and use custom IExtensionGrantValidator.
I manged to invoke the custom IExtensionGrantValidator with approved grant, but client always gets invalid_grant error.
For some reason the set operation ofd Result (property of ExtensionGrantValidationContext) always fails (overriding the Error value returns the overrided value to client).
This is CustomGrantValidator Code:
public class CustomGrantValidator : IExtensionGrantValidator
{
public string GrantType => "grant-name";
public Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result
}
}
Startup.cs contains this line:
services.AddTransient<IExtensionGrantValidator, CustomGrantValidator>();
And finally client's code:
var httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:5000") };
var disco = await httpClient.GetDiscoveryDocumentAsync("http://localhost:5000");
var cReq = await httpClient.RequestTokenAsync(new TokenRequest
{
GrantType = "grant-name",
Address = disco.TokenEndpoint,
ClientId = clientId,// client Id taken from appsetting.json
ClientSecret = clientSecret, //client secret taken from appsetting.json
Parameters = new Dictionary<string, string> { { "hash", hash } }
});
if (cReq.IsError)
//always getting 'invalid_grant' error
throw InvalidOperationException($"{cReq.Error}: {cReq.ErrorDescription}");
The below codes works on my environment :
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var hash = context.Request.Raw["hash"]; //extract hash from request
var result = string.IsNullOrEmpty(hash) ?
new GrantValidationResult(TokenRequestErrors.InvalidRequest) :
new GrantValidationResult(hash, GrantType);
context.Result = result;
return;
}
Don't forget to register the client to allow the custom grant :
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = { "grant-name" },
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
I got the same issue and found the answer from #Sarah Lissachell, turn out that I need to implement the IProfileService. This interface has a method called IsActiveAsync. If you don't implement this method, the answer of ValidateAsync will always be false.
public class IdentityProfileService : IProfileService
{
//This method comes second
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
//IsActiveAsync turns out to be true
//Here you add the claims that you want in the access token
var claims = new List<Claim>();
claims.Add(new Claim("ThisIsNotAGoodClaim", "MyCrapClaim"));
context.IssuedClaims = claims;
}
//This method comes first
public async Task IsActiveAsync(IsActiveContext context)
{
bool isActive = false;
/*
Implement some code to determine that the user is actually active
and set isActive to true
*/
context.IsActive = isActive;
}
}
Then you have to add this implementation in your startup page.
public void ConfigureServices(IServiceCollection services)
{
// Some other code
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddAspNetIdentity<Users>()
.AddInMemoryApiResources(config.GetApiResources())
.AddExtensionGrantValidator<CustomGrantValidator>()
.AddProfileService<IdentityProfileService>();
// More code
}

OpenIddict Samples - Add 'unique-name' to the id_token

I am playing around with the RefreshFlow sample of OpenIddict-Samples. It works great. I notice in the Angular models there is a ProfileModel that is populated from the JWT_Decode of the id_token:
export interface ProfileModel {
sub: string;
jti: string;
useage: string;
at_hash: string;
nbf: number;
exp: number;
iat: number;
iss: string;
unique_name: string;
email_confirmed: boolean;
role: string[];
}
I can't see where on the server the unique_name is being populated. I have a requirement for this field and tried applying the value here:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange([ModelBinder(typeof(OpenIddictMvcBinder))] OpenIdConnectRequest request)
{
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Validate the username/password parameters and ensure the account is not locked out.
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (!result.Succeeded)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
{ "unique_name", "hello World!" }
});
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user, properties);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
Is this where I need to add it? I previously rolled my own token creator using JwtSecureDataFormat : ISecureDataFormat and added the field as a property.
How can I add it with OpenIddict/ASOS?
Thanks!
So I figured out how to achieve mostly what I wanted!!
I really didn't need to specifically add 'unique_name' to the token but simply add more claims than what the standard Identity framework adds for you.
This is how I did it:
Create a custom SignInManager:
public class OpenIdictSignInManager<TUser> : SignInManager<TUser> where TUser : IdentityUser
{
public OpenIdictSignInManager(
UserManager<TUser> userManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<TUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<TUser>> logger,
IAuthenticationSchemeProvider schemes) : base(userManager,
contextAccessor,
claimsFactory,
optionsAccessor,
logger,
schemes)
{
}
public override async Task<ClaimsPrincipal> CreateUserPrincipalAsync(TUser user)
{
var principal = await base.CreateUserPrincipalAsync(user);
var identity = (ClaimsIdentity)principal.Identity;
identity.AddClaim(new Claim(OpenIdConnectConstants.Claims.EmailVerified, user.EmailConfirmed.ToString().ToLower()));
return principal;
}
}
Then applied the new SignInManager to the startup.cs configuration:
// Register the Identity services.
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders()
.AddSignInManager<OpenIdictSignInManager<ApplicationUser>>();
Then added a claim destination when creating the ticket in AuthorizationController:
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
continue;
}
var destinations = new List<string>();
// Identity Token destinations only
if (new List<string>
{
OpenIdConnectConstants.Claims.EmailVerified
}.Contains(claim.Type))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
claim.SetDestinations(destinations);
continue;
}
destinations.Add(OpenIdConnectConstants.Destinations.AccessToken);
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
(claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
(claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(destinations);
}
It took me a few days of digging through code and googling to come up with this approach so I thought I'd share and hope it helps someone else out :)

Basic Authentication Middleware with OWIN and ASP.NET WEB API

I created an ASP.NET WEB API 2.2 project. I used the Windows Identity Foundation based template for individual accounts available in visual studio see it here.
The web client (written in angularJS) uses OAUTH implementation with web browser cookies to store the token and the refresh token. We benefit from the helpful UserManager and RoleManager classes for managing users and their roles.
Everything works fine with OAUTH and the web browser client.
However, for some retro-compatibility concerns with desktop based clients I also need to support Basic authentication. Ideally, I would like the [Authorize], [Authorize(Role = "administrators")] etc. attributes to work with both OAUTH and Basic authentication scheme.
Thus, following the code from LeastPrivilege I created an OWIN BasicAuthenticationMiddleware that inherits from AuthenticationMiddleware.
I came to the following implementation. For the BasicAuthenticationMiddleWare only the Handler has changed compared to the Leastprivilege's code. Actually we use ClaimsIdentity rather than a series of Claim.
class BasicAuthenticationHandler: AuthenticationHandler<BasicAuthenticationOptions>
{
private readonly string _challenge;
public BasicAuthenticationHandler(BasicAuthenticationOptions options)
{
_challenge = "Basic realm=" + options.Realm;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var authzValue = Request.Headers.Get("Authorization");
if (string.IsNullOrEmpty(authzValue) || !authzValue.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var token = authzValue.Substring("Basic ".Length).Trim();
var claimsIdentity = await TryGetPrincipalFromBasicCredentials(token, Options.CredentialValidationFunction);
if (claimsIdentity == null)
{
return null;
}
else
{
Request.User = new ClaimsPrincipal(claimsIdentity);
return new AuthenticationTicket(claimsIdentity, new AuthenticationProperties());
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode == 401)
{
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
Response.Headers.AppendValues("WWW-Authenticate", _challenge);
}
}
return Task.FromResult<object>(null);
}
async Task<ClaimsIdentity> TryGetPrincipalFromBasicCredentials(string credentials,
BasicAuthenticationMiddleware.CredentialValidationFunction validate)
{
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(credentials));
}
catch (FormatException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
return null;
}
var username = pair.Substring(0, ix);
var pw = pair.Substring(ix + 1);
return await validate(username, pw);
}
Then in Startup.Auth I declare the following delegate for validating authentication (simply checks if the user exists and if the password is right and generates the associated ClaimsIdentity)
public void ConfigureAuth(IAppBuilder app)
{
app.CreatePerOwinContext(DbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
Func<string, string, Task<ClaimsIdentity>> validationCallback = (string userName, string password) =>
{
using (DbContext dbContext = new DbContext())
using(UserStore<ApplicationUser> userStore = new UserStore<ApplicationUser>(dbContext))
using(ApplicationUserManager userManager = new ApplicationUserManager(userStore))
{
var user = userManager.FindByName(userName);
if (user == null)
{
return null;
}
bool ok = userManager.CheckPassword(user, password);
if (!ok)
{
return null;
}
ClaimsIdentity claimsIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
return Task.FromResult(claimsIdentity);
}
};
var basicAuthOptions = new BasicAuthenticationOptions("KMailWebManager", new BasicAuthenticationMiddleware.CredentialValidationFunction(validationCallback));
app.UseBasicAuthentication(basicAuthOptions);
// Configure the application for OAuth based flow
PublicClientId = "self";
OAuthOptions = new OAuthAuthorizationServerOptions
{
TokenEndpointPath = new PathString("/Token"),
Provider = new ApplicationOAuthProvider(PublicClientId),
//If the AccessTokenExpireTimeSpan is changed, also change the ExpiresUtc in the RefreshTokenProvider.cs.
AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
AllowInsecureHttp = true,
RefreshTokenProvider = new RefreshTokenProvider()
};
// Enable the application to use bearer tokens to authenticate users
app.UseOAuthBearerTokens(OAuthOptions);
}
However, even with settings the Request.User in Handler's AuthenticationAsyncCore method the [Authorize] attribute does not work as expected: responding with error 401 unauthorized every time I try to use the Basic Authentication scheme.
Any idea on what is going wrong?
I found out the culprit, in the WebApiConfig.cs file the 'individual user' template inserted the following lines.
//// Web API configuration and services
//// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
Thus we also have to register our BasicAuthenticationMiddleware
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new HostAuthenticationFilter(BasicAuthenticationOptions.BasicAuthenticationType));
where BasicAuthenticationType is the constant string "Basic" that is passed to the base constructor of BasicAuthenticationOptions
public class BasicAuthenticationOptions : AuthenticationOptions
{
public const string BasicAuthenticationType = "Basic";
public BasicAuthenticationMiddleware.CredentialValidationFunction CredentialValidationFunction { get; private set; }
public BasicAuthenticationOptions( BasicAuthenticationMiddleware.CredentialValidationFunction validationFunction)
: base(BasicAuthenticationType)
{
CredentialValidationFunction = validationFunction;
}
}

MVC 4 application does not go to default page after login

I have created an MVC 4 application which targets .NET 4.0. After deploying to my production server, it will show the login page but will not redirect to the default page. However, when I add debugging, I can see that the authentication process works but then the error I am getting is an error that says it can't find my View for my Error Page and then shows my Error Page. It just seems that it will not go to my "Home/Index" page - even when I remove the authorize attribute. Of course the application works in development. Additionally, it will not go to my register page or forgot login page.
My Login Controller looks like this:
[HttpPost]
[AllowAnonymous]
[ValidateAntiforgeryToken]
public ActionResult Login(LoginViewModel model, string returnUrl)
{
if(ModelStat.IsValid && _userService.Login(model.UserId, model.Password))
{
var user = _userService.GetUser(model.UserId);
var loggedInUser = new LoggedInUser
{
// Build the user for custom IPrincipal
};
var userData = JsonConvert.SerializeObject(loggedInUser);
var compressData = StringCompression.Compress(userData);
var authTicket = new FormsAuthenticationTicket(
1,
user.UserId,
DateTime.Now,
DateTime.Now.AddHours(1),
false,
compressData);
var encTicket = FormsAuthentication.Encrypt(authTicket);
if(encTicket != null)
{
var faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket)
{
HttpOnly = true
};
Response.Cookies.Add(faCookie);
}
user.LastActivityDate = DateTime.Now;
user.LastLoginDate = DateTime.Now;
_userService.UpdateUser(user);
_uow.Commit();
return Url.IsLocalUrl(returnUrl) ? (ActionResult)Redirect(returnUrl) : RedirectToAction("Index", "Home");
}
return View(model);
and in my Global.asax:
protected void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if(authCookie != null)
{
var decompressedData = StringCompression.Decompress(authTicket.UserData);
var loggedInUser = JsonConvert.DesrializeObject<LoggedInUser>(decompressedData);
var currrentUser = new CustomPrincipal(authTicket.Name)
{
// Build the CustomPrincipal from the loggedInUser
};
if(HttpContext.Current.User.Identity.IsAuthenticated)
{
HttpContext.Current.User = currentUser;
}
}
}
I hope that this is enough to give someone an idea of what I may be doing wrong. Somehow I feel that it is something small that I am missing. Thanks in advance.
~InDireStraits
Update:
After more troubleshooting, it would seem that the issue may have something to do with the fact that I am using a BaseController for specifying permissions but I am still baffled as to why the application works as intended in my development environment but not in production. To verify my IIS settings I installed the default MVC4 App to production which does not have .NET 4.5, and it runs. I am using VS 2012 so I do have 4.5. Could I somehow be introducing .NET 4.5 classes or functionality even if this targets .NET 4.0? At any rate, here is my BaseController code:
public class BaseController: Controller
{
private string _actionKey;
private const string PermisisionList = "permissionList";
private Dictionary<string, string> _requiredActionPermissions;
private static readonly IControllerActionService<ControllerAction> _actionService;
protected new CustomPrincipal User
{
get
{
return HttpContext.User as CustomPrincipal;
}
}
public BaseController(IControllerActionService<ControllerAction> actionService)
{
_actionService = actionService;
}
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Check to see if the PermissionList is loaded and load if necessary
if(!CacheLayer.Exists(PermissionList))
{
_requiredActionPermissions = _actionService.GetControllerActionDictionary();
CacheLayer.Add(_requiredActionPermissions, PermissionList);
}
else
{
_requiredActionPermission = CacheLayer.Get<Dictionary<string, string>>(PermissionList);
}
// Get the Controller/Action of the current request
_actionKey = string.Format("{0}-{1}", filterContext.ActionDescriptor.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor.ActionName);
// If the user is authenticated, grab the permissions
if(filterContext.HttpContext.User.Identity.IsAuthenticated)
{
var userPermissions = User.Permissions;
if(!_requiredActionPermissions.Values.Any(a=>a.Equals(_actionKey, StringComparison.OrdinalIgnoreCase)))
{
return;
}
if(userPermissions.Contains(_requiredActionsPermissions.FirstOrDefault(x=>x.Value == _actionKey).Key))
{
return;
}
filterContext.Result = new RedirectResult("~/Error/ErrorUnauthorized");
return;
}
if(!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if(!_requiredActionPermissions.Values.Any(a=>a.Equals(_actionKey, StringComparison.OrdinalIgnoreCase)))
{
return;
}
}
if(filterContext.HttpContext.Request.Url == null)
{
return;
}
if(filterContext.HttpContext.Request.Url.AbsolutePath == FormsAuthentication.LoginUrl)
{
return;
}
var redirectUrl = string.Format("?returnUrl={0}", filterContext.HttpContext.Request.Url.PathAndQuery);
filterContext.HttpContext.Response.Redirect(FormsAuthentication.LoginUrl + redirectUrl, true);
}
UPDATE 2: Installed .NET 4.52 on Staging Server and the application now works as intended. The problem is that I will not be able to install this on the production server. I don't understand what it is that 4.5 is fixing that 4.0 does not seem to facilitate. HELLLLLLPPP!!!!!!
The answer can be found here. To summarize, I added an extra parameter to my route config that worked in 4.5 but not in 4.0. Will follow up on the linked question. Thanks