gRPC IdentityServer4 Error: Can't sign in with google auth

I'm using Blazor WebAssembly with gRPC and i'm new to Identity Server 4 and trying to implement google sign-in. I already followed the tutorial in the docs but when i tried to load the website, the console gave 2 errors like below. I searched many StackOverflow posts and GitHub issues similiar to this error and it didn't really helped me. My guess is that the error is in the server side because it happens when the website is loading.

Access to XMLHttpRequest at 'https://localhost:5000/signin-google/.well-known/openid-configuration' from origin 'https://localhost:5001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

AuthenticationService.js:1 GET https://localhost:5000/signin-google/.well-known/openid-configuration net::ERR_FAILED

The error shows that it has been blocked by CORS policy even though i already allowed all website url to access it(for testing purposes) and when i'm trying to sign-in with google, i got redirected to a failed login url that says network error. Here's the code.
BackEnd/Startup.cs

namespace BackEnd
    {
        public class Startup
        {
            // This method gets called by the runtime. Use this method to add services to the container.
            // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddGrpc();
    
            
    
                services.AddDbContext<UserDbContext>(options => options.UseInMemoryDatabase("UserDatabase"));
    
                services.AddCors(o => o.AddPolicy("AllowAll", builder =>
                {
                    builder.AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
                }));
    
                services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
                    .AddRoles<IdentityRole>()
                    .AddEntityFrameworkStores<UserDbContext>();
    
                services.AddIdentityServer()
                    .AddInMemoryIdentityResources(ServerConfiguration.IdentityResources)
                    .AddInMemoryApiResources(ServerConfiguration.ApiResources)
                    .AddInMemoryApiScopes(ServerConfiguration.ApiScopes)
                    .AddInMemoryClients(ServerConfiguration.Clients)
                    // .AddApiAuthorization<ApplicationUser, UserDbContext>()
                    .AddTestUsers(ServerConfiguration.TestUsers);
    
                JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
    
                services.AddTransient<IProfileService, ProfileService>();
    
                services.AddAuthentication(options => {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
                })
                    .AddGoogle("Google", options =>
                    {
                        options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
    
                        options.ClientId = <confidential>;
                        options.ClientSecret = <confidential>;
                    })
                    .AddCookie("Cookies")
                    .AddOpenIdConnect("oidc", options =>
                    {
                        options.Authority = "https://localhost:5000";
    
                        options.ClientId = "499675830263-ldcg4fm7kcbjlt48tpaffqdbfnskmi8v.apps.googleusercontent.com";
                        options.ResponseType = "code";
    
                        options.SaveTokens = true;
    
                        options.Scope.Add("protectedScope");
                        options.Scope.Add("offline_access");
                        options.Scope.Add("role");
                        options.ClaimActions.MapJsonKey("role", "role", "role");
                        options.TokenValidationParameters.RoleClaimType = "role";
                    });
    
                services.AddAuthorization();
            }
    
            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
    
                app.UseRouting();
    
                app.UseCors();
    
                app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
    
                app.UseIdentityServer();
    
                app.UseAuthentication();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapGrpcService<GreeterService>().RequireCors("AllowAll");
    
                    endpoints.MapGrpcService<UserService>().RequireCors("AllowAll");
    
                    endpoints.MapGet("/", async context =>
                    {
                        await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
                    });
                });
            }
        }
    }

BackEnd/ServerConfiguration.cs

namespace BackEnd
{
    public static class ServerConfiguration
    {
        public static List<IdentityResource> IdentityResources { 
            get 
            {
                List<IdentityResource> idResources = new List<IdentityResource>()
                {
                    new IdentityResources.OpenId(),
                    new IdentityResources.Profile(),
                    new IdentityResources.Email(),
                    new IdentityResource("roles", "User roles", new List<string> { "role" })
                };
                return idResources;
            }
        }
        public static List<ApiScope> ApiScopes {
            get
            {
                List<ApiScope> apiScopes = new List<ApiScope>();
                apiScopes.Add(new ApiScope("protectedScope", "Protected Scope"));
                return apiScopes;
            }
        }
        public static List<ApiResource> ApiResources { 
            get
            {
                ApiResource userApiResource = new ApiResource("toDoWebApiResource", "Todo Web Api")
                {
                    Scopes = { "protectedScope" },
                    UserClaims = 
                    {
                        "openid",
                        "email",
                        "profile",
                        "role"
                    }
                };
                List<ApiResource> apiResources = new List<ApiResource>();
                apiResources.Add(userApiResource);

                return apiResources;
            }
        }
        public static List<Client> Clients { 
            get
            {
                Client client = new Client()
                {
                    ClientId = "499675830263-ldcg4fm7kcbjlt48tpaffqdbfnskmi8v.apps.googleusercontent.com",
                    ClientName = "client 1",
                    RequireClientSecret = false,
                    RequirePkce = true,
                    AllowedCorsOrigins = { "https://localhost:5001" },
                    AllowedGrantTypes = GrantTypes.Code,
                    RedirectUris = { "https://localhost:5001/authentication/login-callback" },
                    PostLogoutRedirectUris = { "https://localhost:5001/authentication/logout-callback" },
                    AllowOfflineAccess = true,
                    AllowedScopes = new List<string>{
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        "protectedScope"
                    }
                };
                List<Client> clients = new List<Client>();
                clients.Add(client);

                return clients;
            }
        }
        public static List<TestUser> TestUsers { 
            get
            {
                TestUser user1 = new TestUser()
                {
                    SubjectId = "2f47f8f0-bea1-4f0e-ade1-88533a0eaf57",
                    Username = "John",
                    Claims = new List<Claim>()
                    {
                        new Claim("role", "SignedInUser"),
                        new Claim("email", "johnsmith@gmail.com"),
                        new Claim("picture", "https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.business2community.com%2Fsocial-media%2Fimportance-profile-picture-career-01899604&psig=AOvVaw2LC5T-WZMYnHD9I7PeK7lT&ust=1615219065948000&source=images&cd=vfe&ved=2ahUKEwip1caGxp7vAhV1NbcAHd_2BFwQjRx6BAgAEAc")
                    }
                };
                List<TestUser> testUsers = new List<TestUser>();
                testUsers.Add(user1);

                return testUsers;
            }
        }
    }
}

FrontEnd/wwwroot/appsettings.json

{
    "Authentication":{
        "Google": {
            "Authority": "https://localhost:5000",
            "ClientId": <confidential>,
            "ClientSecret": <confidential>,
            "DefaultScopes": [
                "email",
                "profile",
                "openid"
            ],
            "PostLogoutRedirectUri": "https://localhost:5001/authentication/logout-callback",
            "RedirectUri": "https://localhost:5001/authentication/login-callback",
            "ResponseType": "code"
        }
    }
}

2 answers

  • answered 2021-03-09 08:21 Tore Nestenius

    You use the Google authentication handler, like:

    .AddGoogle("Google", options =>
    {
        //Configuration
    }).
    

    And it will automatically by default listen for incoming requests from Google on this URL /signin-google

    so that is automatically done for you.

    For the client that authenticates to IdentityServer, using AddOpenIDConnect, you typicall want to have https://localhost:5001/signin-oidc as the redirect URL in the client definition in IdentityServer.

    In your example, you also have AuthenticationService.js:1 GET https://localhost:5000/signin-google/.well-known/openid-configuration net::ERR_FAILED

    Are you not using 5001 for HTTPS and 5000 for HTTP?

    If localhost:5001 is your IdentityServer, then you would access the discovery document on AuthenticationService.js:1 GET https://localhost:5000/.well-known/openid-...

    The URL localhost:5000/signin-google is only used if Google contacts you, and your client should never call that endpoint.

    Perhaps if you better explained your architecture, then I can provide a better answer because right now I think you are mixing up IdentityServer, Google and your client.

  • answered 2021-03-09 08:24 TheNoobProgrammer

    I found an answer to this problem, what i need to do is quite simple. I create the gRPC server without Identity Server UI and configuration, i just need to add the UI for it to work.
    Install IdentityServer4 UI template

    dotnet new -i IdentityServer4.Templates
    

    Add IdentityServer UI

    dotnet new is4ui
    

    Add the controller

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGrpcService<GreeterService>().RequireCors("AllowAll");
    
        endpoints.MapGrpcService<UserService>().RequireCors("AllowAll");
    
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
        });
    
        // Add this
        endpoints.MapDefaultControllerRoute().RequireAuthorization();
    });