jenson-button-event
jenson-button-event

Reputation: 18961

Limit user authorization to my Google domain

It should be possible to limit Google API OAuth2 requests to a specific google domain. It used to be possible by hacking on the end &hd=mydomain.com to the request. Using the new MVC auth stuff it seems no longer possible. Any ideas how?

 public class AppFlowMetadata : FlowMetadata
    {
        private static readonly IAuthorizationCodeFlow flow =
            new AppGoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
            {
                ClientSecrets = new ClientSecrets
                {
                    ClientId = "***.apps.googleusercontent.com",
                    ClientSecret = "******"
                },
                Scopes = new[] { DriveService.Scope.Drive },
                DataStore = new FileDataStore(HttpContext.Current.Server.MapPath("~/App_Data"), true) ,
            });  

        public override string GetUserId(Controller controller)
        {
            // In this sample we use the session to store the user identifiers.
            // That's not the best practice, because you should have a logic to identify
            // a user. You might want to use "OpenID Connect".
            // You can read more about the protocol in the following link:
            // https://developers.google.com/accounts/docs/OAuth2Login.
            var user = controller.Session["user"];
            if (user == null)
            {
                user = Guid.NewGuid();
                controller.Session["user"] = user;
            }
            return user.ToString();

        }

        public override IAuthorizationCodeFlow Flow
        {
            get { return flow; }
        }
    }

public class AppGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
    {
        public AppGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }

        public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(String redirectUri)
        {

            var authorizeUri = new Uri(AuthorizationServerUrl).AddQuery("hd", "ourgoogledomain.com"); //is not in the request
            var authUrl = new GoogleAuthorizationCodeRequestUrl(authorizeUri)
            {
                ClientId = ClientSecrets.ClientId,
                Scope = string.Join(" ", Scopes),
                RedirectUri = redirectUri,
                //AccessType = "offline",
               // ApprovalPrompt = "force"
            };
            return authUrl;
        }
    }

Upvotes: 1

Views: 2157

Answers (5)

Anson Goldade
Anson Goldade

Reputation: 29

I found this post when searching for a solution to specify the hosted domain with OpenID Connect integration to Google. I was able to get it working using the Google.Apis.Auth.AspNetCore package and the following code.

In Startup.cs

services.AddGoogleOpenIdConnect(options =>
  {
    options.ClientId = "*****";
    options.ClientSecret = "*****";
    options.SaveTokens = true;
    options.EventsType = typeof(GoogleAuthenticationEvents);
  });
services.AddTransient(provider => new GoogleAuthenticationEvents("example.com"));

Don't forget app.UseAuthentication(); in the Configure() method of Startup.cs.

Then the authentication events class

public class GoogleAuthenticationEvents : OpenIdConnectEvents
{
  private readonly string _hostedDomain;

  public GoogleAuthenticationEvents(string hostedDomain)
  {
    _hostedDomain = hostedDomain;
  }

  public override Task RedirectToIdentityProvider(RedirectContext context)
  {
    context.ProtocolMessage.Parameters.Add("hd", _hostedDomain);
    return base.RedirectToIdentityProvider(context);
  }

  public override Task TicketReceived(TicketReceivedContext context)
  {
    var email = context.Principal.FindFirstValue(ClaimTypes.Email);
    if (email == null || !email.ToLower().EndsWith(_hostedDomain))
    {
      context.Response.StatusCode = 403;
      context.HandleResponse();
    }
    return base.TicketReceived(context);
  }
}

Upvotes: 0

Piotr Włoda
Piotr Włoda

Reputation: 31

@AMH, to do in simplest way you should create your own Google Provider, override method ApplyRedirect and append additional parameter like hd to address which will be using to redirect to a new google auth page:

public class GoogleAuthProvider : GoogleOAuth2AuthenticationProvider
{
    public override void ApplyRedirect(GoogleOAuth2ApplyRedirectContext context)
    {
        var newRedirectUri = context.RedirectUri;
        newRedirectUri += string.Format("&hd={0}", "your_domain.com");

        context.Response.Redirect(newRedirectUri);
    }
}

After that just link new provider to your options:

app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
    ClientId = "your id",
    ClientSecret = "your secret",
    Provider = new GoogleAuthProvider(),
});

Upvotes: 3

Jenya Y.
Jenya Y.

Reputation: 3230

With the updated for .NET core package previous answers are no longer suitable. Fortunately in the new implementation there is a way to hook into authentication events to perform such task.

You will need a class that will handle 2 events - the one that fired before you go to Google and the one for when coming back. In first you limit which domain can be used to sign-in and in the second you ensure that the email with the right domain was in fact used for signin:

internal class GoogleAuthEvents : OAuthEvents
{
    private string _domainName;

    public GoogleAuthEvents(string domainName)
    {
        this._domainName = domainName?.ToLower() ?? throw new ArgumentNullException(nameof(domainName));
    }

    public override Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context)
    {
        return base.RedirectToAuthorizationEndpoint(new OAuthRedirectToAuthorizationContext(
            context.HttpContext,
            context.Options,
            context.Properties,
            $"{context.RedirectUri}&hd={_domainName}"));
    }

    public override Task TicketReceived(TicketReceivedContext context)
    {
        var emailClaim = context.Ticket.Principal.Claims.FirstOrDefault(
                c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");

        if (emailClaim == null || !emailClaim.Value.ToLower().EndsWith(_domainName))
        {
            context.Response.StatusCode = 403; // or redirect somewhere
            context.HandleResponse();
        }

        return base.TicketReceived(context);
    }
}

and then you need to pass this "events handler" to the middleware via GoogleOptions class:

app.UseGoogleAuthentication(new GoogleOptions
{
    Events = new GoogleAuthEvents(Configuration["Authentication:Google:LimitToDomain"])
})

Upvotes: 3

William Denniss
William Denniss

Reputation: 16336

Passing a hd parameter is indeed the correct way to limit users to your domain. However, it is important that you verify that the user does actually belong to that hosted domain. I see in your answer that you figured out how to add this parameter back in to your request, so I will address the second part of this.

The issue is that the user can actually modify the requested URL in their client and remove the hd parameter! So while it's good to pass this parameter for the best UI for your users, you need to also verify that authenticated users do actually belong to that domain.

To see which hosted Google Apps for Work domain (if any) the user belongs to, you must include email in the list of scopes that you authorize. Then, do one of the following:

Option 1. Verify the ID Token.

When you exchange your code for an access token, the token endpoint will also return an ID Token in the id_token param (assuming you include an identity scope in your request such as email). If the user is part of a hosted domain, a hd claim will be present, you should check that it is present, and matches what you expect.

You can read more about ID tokens on Google's OpenID Connect docs (including some links to sample code and libraries to help you decode them). This tool can decode ID Tokens during testing.

Option 2. Call UserInfo

Once you have the OAuth Access Token, perform a GET request to https://www.googleapis.com/plus/v1/people/me/openIdConnect with the Access Token in the header. It will return a JSON dictionary of claims about the user. If the user is part of a hosted domain, a hd claim will be present, you should check that it is present, and matches what you expect.

Read more in the documentation for Google's UserInfo endpoint.

The main difference between Option 1 and Option 2 is that with the ID Token, you avoid another HTTP round-trip to the server making it faster, and less error-prone. You can try out both these options interactively using the OAuth2 Playground.

Upvotes: 3

jenson-button-event
jenson-button-event

Reputation: 18961

Having downloaded the source, I was able to see it is easy to subclass the request object, and add custom parameters:

    public class GoogleDomainAuthorizationCodeRequestUrl : GoogleAuthorizationCodeRequestUrl
    {
        /// <summary>
        /// Gets or sets the hosted domain. 
        /// When you want to limit authorizing users from a specific domain 
        /// </summary>
        [Google.Apis.Util.RequestParameterAttribute("hd", Google.Apis.Util.RequestParameterType.Query)]
        public string Hd { get; set; }

        public GoogleDomainAuthorizationCodeRequestUrl(Uri authorizationServerUrl) : base(authorizationServerUrl)
        {
        }
    }

    public class AppGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
    {
        public AppGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }

        public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(String redirectUri)
        {
            var authUrl = new GoogleDomainAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
            {
                Hd = "mydomain.com",
                ClientId = ClientSecrets.ClientId,
                Scope = string.Join(" ", Scopes),
                RedirectUri = redirectUri
            };

            return authUrl;
        }
    }

Upvotes: 1

Related Questions