17
votes

After adding Authentication functionality using Identity Server 4 with ASP.NET Identity, I'm planning to add the Google Provider so users can also login with their google+ account. I'm using Angular as my front-end and ASP.NET Web Api (Core) as back-end.

// Login client
public login(email: string, password: string): Observable<any> {
    let body: any = this.encodeParams({ /* cliend_id, grant_type, username, password, scope */ });

    return this.http.post("http://localhost:64023/connect/token", body, this.options)
        .map((res: Response) => {
            const body: any = res.json();
                if (typeof body.access_token !== "undefined") {
                    // Set localStorage with id_token,..
                }
        }).catch((error: any) => { /**/ );
}

// Register Web API
[HttpPost("Create")]
[AllowAnonymous]
public async Task<IActionResult> Create([FromBody]CreateUserViewModel model)
{
    var user = new ApplicationUser
    {
        FirstName = model.FirstName,
        LastName = model.LastName,
        AccessFailedCount = 0,
        Email = model.Email,
        EmailConfirmed = false,
        LockoutEnabled = true,
        NormalizedEmail = model.Email.ToUpper(),
        NormalizedUserName = model.Email.ToUpper(),
        TwoFactorEnabled = false,
        UserName = model.Email
    };

    var result = await _userManager.CreateAsync(user, model.Password);

    if (result.Succeeded)
    {
        await addToRole(model.Email, "user");
        await addClaims(model.Email);
    }

    return new JsonResult(result);
}

// Identity Server Startup 
app.UseGoogleAuthentication(new GoogleOptions
{
    AuthenticationScheme = "Google",
    DisplayName = "Google",
    SignInScheme = "Identity.External",
    // ClientId, ClientSecret,..
});

After a user login, the localStorage gets set and I'm able to protect the secure Controllers. For the Google Provider I added an extra button and following methods:

initGoogleAPI() {
    let self = this;
    gapi.load('auth2', function () {
        self.auth2 = gapi.auth2.init({ /* client_id, cookiepolicy, scope */ });
        self.externalLogin(document.getElementById('google-button'));
    });
}

externalLogin(element) {
    let self = this;
    this.auth2.attachClickHandler(element, {},
        function (googleUser) {
            // retrieved the id_token, name, email,...
        }, function (error) {
        alert(JSON.stringify(error, undefined, 2));
    });
}

I have found a few solutions but only for MVC-applications and not for a SPA using a clientside framework. What steps do I need to take next for the external login to work? Is there need to create a new record in the AspNetUsers Table when a user signs in for the first time using the external provider?

3

3 Answers

4
votes

You can check this repository, you can ignore the ids4 server project and check the angular client you should used an openid client to do this, then the client is redirected to the ids4 project login page where you login and it returns a token which you save it so you can use it later

https://github.com/nertilpoci/Aspnetcore-identityserver4-webapi-angular

https://github.com/nertilpoci/Aspnetcore-identityserver4-webapi-angular/tree/master/ClientApp

2
votes

The logins should be handled by the identityServer4. It should return a similar token to the web app regardless of a local login or 3rd party.

If you need user data in your Web API backend it should be passed along as claims.

IdentityServer4 Quick starts might be helpful for your code, they have an example for External logins which you add to your IdentityServer and how to do a login flow from a JavaScript application.

1
votes

I wanted to post some thoughts here as I have often struggled with this from a perfect user-experience point of view. I was able to implement this a long time ago by using a manual approach. I am not sure if this is an Identity Server thing, but nonetheless, it can be implemented as a part of the ASP.NET Identity system in conjunction with Identity Server.

NOTE: The ideal scenario is that you let a user login/register on Google/FB by directing them to the Identity provider's login page (in this case Identity Server). They login to Google or FB there and then are redirected back to the application by Identity Server. This is the Implicit or PKCE flow. PKCE has additional steps, but claims processing etc. is all done by IdSrv code. This is what you see most widely documented.

MOST OFTEN REQUESTED SCENARIO

I've often had clients request that we simply use the Javascript Google and FB logins via their corresponding Javascript libraries. This is the scenario you're talking about. The flow in this case would be something like so:

  1. The user arrives at your angular or react app page

  2. They sign-in using Google/FB - this will give us the "External Access Token". i.e. the token for Google and FB

  3. You can then take this token information and pass it with any additional information (such as client id, tenant id etc.) to a new API method you can create.

The API can have a method called "POST api/local-token" which will internally validate the External Access Token for authenticity. If the information checks out, you generate a new application specific bearer access Token for the user and pass it as a response back to the Javascript app. The JS app can then store both the External Access Token (for FB/Google API requests), the application's Access Token (for your application API requests) in cookie/local storage and then continue from there on.

I have been able to do this quite easily.

If you want to generate the access token, it's pretty str8 forward, here's an example from my code. This will literally generate a response similar to what Identity Server would generate:

  public static async Task<JObject> GenerateLocalAccessTokenResponse(string userName, string role, string userId, string clientId, string provider)
    {

        var tokenExpiration = TimeSpan.FromDays(1);

        var identity = new ClaimsIdentity(OAuthDefaults.AuthenticationType);

        identity.AddClaim(new Claim(ClaimTypes.Name, userName));
        identity.AddClaim(new Claim("ClientId", clientId));
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId));
        identity.AddClaim(new Claim(ClaimTypes.Role, role));


        var data = new Dictionary<string, string>
        {
            {"userName", userName},
            {"client_id", clientId},
            {"role", role},
            {"provider", provider},
            {"userId", userId}
        };

        var props = new AuthenticationProperties(data);

        var ticket = new AuthenticationTicket(identity, props);

        var accessToken = Startup.OAuthOptions.AccessTokenFormat.Protect(ticket);

        var tokenResponse = new JObject(
            new JProperty("userName", userName),
            new JProperty("client_id", clientId),
            new JProperty("role", role),
            new JProperty("provider", provider),
            new JProperty("userId", userId),
            new JProperty("access_token", accessToken),
            new JProperty("token_type", "bearer"),
            new JProperty("expires_in", tokenExpiration.TotalSeconds.ToString()),
            new JProperty(".issued", ticket.Properties.IssuedUtc.ToString()),
            new JProperty(".expires", ticket.Properties.ExpiresUtc.ToString())
            );

        return tokenResponse;
    }