8

I have been through the docs of identityServer4 and I have set it up to use Microsoft Office 365 as a login provider. When the user has logged in I want to make a button where he can allow my app to subscribe to his calendar events using the webhooks api of graph.microsoft.com

The code in startup.cs

app.UseMicrosoftAccountAuthentication(new MicrosoftAccountOptions
{
     AuthenticationScheme = "Microsoft",
     DisplayName = "Microsoft",
     SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme,

     ClientId = "CLIENT ID",
     ClientSecret = "CLIENT SECRET",
     CallbackPath = new PathString("/signin-microsoft"),
     Events = new OAuthEvents
     {
         OnCreatingTicket = context =>
         {
             redisCache.Set("AccessToken", context.AccessToken.GetBytes(), new DistributedCacheEntryOptions
             {
                 AbsoluteExpiration = DateTimeOffset.UtcNow.AddDays(3)
             });
             return Task.FromResult(context);
         }
     }
     Scope =
     {
         "Calendars.Read",
         "Calendars.Read.Shared",
     },
     SaveTokens = true
});

But this is obviously not a viable path to go. I have only done this for testing purposes and to make a PoC of the subscriptions needed.

Now I would like to know if there is a smarter way to communicate with the identityServer that allows me to get this external access token, so that I can use the microsoft api on behalf of my logged in users?

Or is my only option to take the Microsoft AccessToken directly from this OAuthEvent and store it directly in a database, linked to the logged in user?

I really need this, since most of my functionality is based on data from third parties.

Kristian Barrett
  • 3,133
  • 2
  • 22
  • 36

1 Answers1

14

Ok, so I finally got this working. I have created a new project that is using ASP.Net Identity and IdentityServer4 both build on top of ASP.Net Core.

The problem was that I wasn't completely aware of the flow that was used in the external login process.

If you use the boiler plates from both systems you will have an AccountController where the following method will be present:

//
// GET: /Account/ExternalLoginCallback
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
    if (remoteError != null)
    {
        ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}");
        return View(nameof(Login));
    }
    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        return RedirectToAction(nameof(Login));
    }

    // Sign in the user with this external login provider if the user already has a login.
    var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
    if (result.Succeeded)
    {
        await _signInManager.UpdateExternalAuthenticationTokensAsync(info);

        _logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider);
        return RedirectToLocal(returnUrl);
    }
    if (result.RequiresTwoFactor)
    {
        return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl });
    }
    if (result.IsLockedOut)
    {
        return View("Lockout");
    }
    else
    {
        // If the user does not have an account, then ask the user to create an account.
        ViewData["ReturnUrl"] = returnUrl;
        ViewData["LoginProvider"] = info.LoginProvider;
        var email = info.Principal.FindFirstValue(ClaimTypes.Email);
        return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
    }
}

The important part here is the:

await _signInManager.UpdateExternalAuthenticationTokensAsync(info);

This will save your external credentials in the database table associated with your ASP.Net identity. In the table AspNetUserTokens you will now have 3 entries, called something like: access_token, expires_at and token_type.

These are the tokens that we are interested in, that we can use to access the users credentials somewhere else in our application.

To fetch these tokens in the context of a logged in user:

var externalAccessToken = await _userManager.GetAuthenticationTokenAsync(User, "Microsoft", "access_token");

And to fetch them for a user we fetch from the DB we can use:

var user = _userManager.Users.SingleOrDefault(x => x.Id == "myId");
if (user == null)
    return;

var claimsPrincipal = await _signInManager.CreateUserPrincipalAsync(user);
var externalAccessToken = await _userManager.GetAuthenticationTokenAsync(claimsPrincipal, "Microsoft", "access_token");
Kristian Barrett
  • 3,133
  • 2
  • 22
  • 36
  • Hi Kristian, are you using all parts in one asp.net project or have a different endpoint for the IdentityServer? – Amr Elsehemy Jun 12 '17 at 16:25
  • @AmrElsehemy I am hosting the identity server in a separate project. We are utilizing microservices and our other APIs are communicating with the identity server through the Access token validation middleware. Hope this answers your question. – Kristian Barrett Jun 12 '17 at 17:14
  • Thank you, another question, is the identity server hosted in the same endpoint or an external one, which is better to use? – Amr Elsehemy Jun 12 '17 at 17:21
  • @AmrElsehemy I am not exactly sure what it is that you are asking. My IdentityServer only is hosted on a subdomain to our domain. Is it specifically for the external login? If so I would recommend you to follow the standard setup, that is shown on identity servers readthedocs page. – Kristian Barrett Jun 13 '17 at 07:57
  • 1
    @KristianBarrett, How do you retrieve this access token into web api ? – Gopal Zadafiya May 25 '19 at 08:04
  • @GopalZadafiya have you found answer ? – Rémi Lardier Jul 16 '20 at 18:14
  • @Remi Lardier I endup creating another login page using MSAL library – Gopal Zadafiya Jul 16 '20 at 18:44
  • @GopalZadafiya Ok thanks, maybe add routes to IdentityServerApp to get user tokens could be better (example https://theidserver.herokuapp.com/api/swagger/index.html) – Rémi Lardier Jul 16 '20 at 20:00
  • I am unsure what it is you are trying to do? Do you mean how you retrieve these tokens again? You can just fetch them directly from the database as shown above. If you need to send them to another service - then use your internal communication like a rest call between the two services? Maybe I misunderstand. Else create a question with more details and I can try to answer. – Kristian Barrett Jul 17 '20 at 11:30