0

I am wanting to be able to access the UserInfo endpoint /connect/userinfo but when I do so it says 401... implying authentication needed. So I ran into this article that talks about how we can implement it in .net code in order to be able to access the UserInfo Endpoint. So inside of my TokenServices.cs file I did the following however, I am not sure how I am able to get the token itself. I do have a method GetToken() that will retrieve a token but I am not sure if I can use that method for the line that sets Token = token.

public async Task<TokenResponse> GetUserInfoToken(string scope)
        {
            using var client = new HttpClient();
           
            var tokenResponse = await client.GetUserInfoAsync(new UserInfoRequest
            {
                Address = _discoveryDocument.UserInfoEndpoint,
          //      Token = token
            });

            if (tokenResponse.isError)
            {
                _logger.LogError($"Unable to get userinfo token. Error is: {tokenResponse.Error}");
                throw new Exception("Unable to get UserInfo token", tokenResponse.Exception);
            }
            return tokenResponse;
        }

This is the full class file:

namespace WeatherMVC.Services
{
    public class TokenService : ITokenService
    {
        private readonly ILogger<TokenService> _logger;
        private readonly IOptions<IdentityServerSettings> _identityServerSettings;
        private readonly DiscoveryDocumentResponse _discoveryDocument;

        public TokenService(ILogger<TokenService> logger, IOptions<IdentityServerSettings> identityServerSettings)
        {
            _logger = logger;
            _identityServerSettings = identityServerSettings;

            using var httpClient = new HttpClient();
            _discoveryDocument = httpClient.GetDiscoveryDocumentAsync(identityServerSettings.Value.DiscoveryUrl).Result;

            if (_discoveryDocument.IsError)
            {
                logger.LogError($"Unable to get discovery document. Error is: {_discoveryDocument.Error}");
                throw new Exception("Unable to get discovery document", _discoveryDocument.Exception);
            }
        }

        public async Task<TokenResponse> GetToken(string scope)
        {
            using var client = new HttpClient();
            var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            {
                Address = _discoveryDocument.TokenEndpoint,
                ClientId = _identityServerSettings.Value.ClientName,
                ClientSecret = _identityServerSettings.Value.ClientPassword,
                Scope = scope
            });

            if (tokenResponse.IsError)
            {
                _logger.LogError($"Unable to get token. Error is: {tokenResponse.Error}");
                throw new Exception("Unable to get token", tokenResponse.Exception);
            }
            return tokenResponse;
        }

        public async Task<TokenResponse> GetUserInfoToken(string scope)
        {
            using var client = new HttpClient();
           
            var tokenResponse = await client.GetUserInfoAsync(new UserInfoRequest
            {
                Address = _discoveryDocument.UserInfoEndpoint,
          //      Token = token
            });

            if (tokenResponse.isError)
            {
                _logger.LogError($"Unable to get userinfo token. Error is: {tokenResponse.Error}");
                throw new Exception("Unable to get UserInfo token", tokenResponse.Exception);
            }
            return tokenResponse;
        }
    }

Inside of my HomeController I have:

namespace WeatherMVC.Controllers
{
    public class HomeController : Controller
    {
        private readonly ITokenService _tokenService;
        private readonly ILogger<HomeController> _logger;

        public HomeController(ITokenService tokenService, ILogger<HomeController> logger)
        {
            _tokenService = tokenService;
            _logger = logger;
        }

        public IActionResult Index()
        {
            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [Authorize] // 25:44 in youtube video
        public async Task<IActionResult> Weather()
        {
            var data = new List<WeatherData>();
            
            using (var client = new HttpClient())
            {
                var tokenResponse = await _tokenService.GetToken("weatherapi.read");

                client.SetBearerToken(tokenResponse.AccessToken);

                var result = client
                    .GetAsync("https://localhost:5445/weatherforecast")
                    .Result;

                if (result.IsSuccessStatusCode)
                {
                    var model = result.Content.ReadAsStringAsync().Result;

                    data = JsonConvert.DeserializeObject<List<WeatherData>>(model);

                    return View(data);
                }
                else
                {
                    throw new Exception("Unable to get content");
                }
            }
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }

Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "cookie";
                options.DefaultChallengeScheme = "oidc";
            })
                .AddCookie("cookie")
                .AddOpenIdConnect("oidc", options =>
                {
                    options.Authority = Configuration["InteractiveServiceSettings:AuthorityUrl"];
                    options.ClientId = Configuration["InteractiveServiceSettings:ClientId"];
                    options.ClientSecret = Configuration["InteractiveServiceSettings:ClientSecret"];

                    options.ResponseType = "code";
                    options.UsePkce = true;
                    options.ResponseMode = "query";

                    options.Scope.Add(Configuration["InteractiveServiceSettings:Scopes:0"]);
                    options.SaveTokens = true;

                });


            services.Configure<IdentityServerSettings>(Configuration.GetSection("IdentityServerSettings"));
            services.AddSingleton<ITokenService, TokenService>();
        }

WeatherApi Project

Startup.cs

public void ConfigureServices(IServiceCollection services)
        {

            services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication("Bearer", options =>
                {
                    options.ApiName = "weatherapi";
                    options.Authority = "https://localhost:5443";
                });

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "weatherapi", Version = "v1" });
            });
        }

Identity project

namespace identity
{
    public static class Config
    {
        public static List<TestUser> Users
        {
            get
            {
                var address = new
                {
                    street_address = "One Hacker Way",
                    locality = "Heidelberg",
                    postal_code = 69118,
                    country = "Germany"
                };

                return new List<TestUser>
        {
          new TestUser
          {
            SubjectId = "818727",
            Username = "alice",
            Password = "alice",
            Claims =
            {
              new Claim(JwtClaimTypes.Name, "Alice Smith"),
              new Claim(JwtClaimTypes.GivenName, "Alice"),
              new Claim(JwtClaimTypes.FamilyName, "Smith"),
              new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"),
              new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
              new Claim(JwtClaimTypes.Role, "admin"),
              new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
              new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address),
                IdentityServerConstants.ClaimValueTypes.Json)
            }
          },
          new TestUser
          {
            SubjectId = "88421113",
            Username = "bob",
            Password = "bob",
            Claims =
            {
              new Claim(JwtClaimTypes.Name, "Bob Smith"),
              new Claim(JwtClaimTypes.GivenName, "Bob"),
              new Claim(JwtClaimTypes.FamilyName, "Smith"),
              new Claim(JwtClaimTypes.Email, "BobSmith@email.com"),
              new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
              new Claim(JwtClaimTypes.Role, "user"),
              new Claim(JwtClaimTypes.WebSite, "http://bob.com"),
              new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address),
                IdentityServerConstants.ClaimValueTypes.Json)
            }
          }
        };
            }
        }

        public static IEnumerable<IdentityResource> IdentityResources =>
          new[]
          {
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResource
        {
          Name = "role",
          UserClaims = new List<string> {"role"}
        }
          };

        public static IEnumerable<ApiScope> ApiScopes =>
          new[]
          {
        new ApiScope("weatherapi.read"),
        new ApiScope("weatherapi.write"),
          };
        public static IEnumerable<ApiResource> ApiResources => new[]
        {
      new ApiResource("weatherapi")
      {
        Scopes = new List<string> {"weatherapi.read", "weatherapi.write"},
        ApiSecrets = new List<Secret> {new Secret("ScopeSecret".Sha256())},
        UserClaims = new List<string> {"role"}
      }
    };

        public static IEnumerable<Client> Clients =>
          new[]
          {
        // m2m client credentials flow client
        new Client
        {
          ClientId = "m2m.client",
          ClientName = "Client Credentials Client",

          AllowedGrantTypes = GrantTypes.ClientCredentials,
          ClientSecrets = {new Secret("SuperSecretPassword".Sha256())},

          AllowedScopes = {"weatherapi.read", "weatherapi.write"}
        },

        // interactive client using code flow + pkce
        new Client
        {
          ClientId = "interactive",
          ClientSecrets = {new Secret("SuperSecretPassword".Sha256())},

          AllowedGrantTypes = GrantTypes.Code,

          RedirectUris = {"https://localhost:5444/signin-oidc" , "https://localhost:44394/signin-oidc"}, 
          FrontChannelLogoutUri = "https://localhost:5444/signout-oidc",
          PostLogoutRedirectUris = {"https://localhost:5444/signout-callback-oidc"},

          AllowOfflineAccess = true,
          AllowedScopes = {"openid", "profile", "weatherapi.read"},
          RequirePkce = true,
          RequireConsent = true,
          AllowPlainTextPkce = false
        },
          };
    }
}

Is there a specific way I can call the token so I can set Token to that UserInfo token? Any pointers/suggestions would be greatly appreciated!


UPDATED

enter image description here

enter image description here

enter image description here

To access this screen (HomeController -> Weather()) I have to be authorized and it keeps me logged in when I access this page regardless how long the bearer token says it lasts. So why can't I access the /connect/userinfo page?

enter image description here

NoviceCoder
  • 449
  • 1
  • 9
  • 26
  • What is your `GetToken()`?You can try to use it first,and check `tokenResponse.isError`. – Yiyi You Jun 22 '21 at 05:31
  • @YiyiYou `GetToken()` is the same way the curl cmd works to get a token. `tokenResponse.isError` is false. Please check my updated post so you can see what `tokenResponse` holds. – NoviceCoder Jun 22 '21 at 14:03

1 Answers1

2

The use of options.SaveTokens = true; (In AddOpenIDConnect) will save all the tokens in the user cookie and then you can access it using:

var accessToken = await HttpContext.GetTokenAsync("access_token");

Sample code to get all the tokens if provided:

            ViewBag.access_token = HttpContext.GetTokenAsync("access_token").Result;
            ViewBag.id_token = HttpContext.GetTokenAsync("id_token").Result;
            ViewBag.refresh_token = HttpContext.GetTokenAsync("refresh_token").Result;
            ViewBag.token_type = HttpContext.GetTokenAsync("token_type").Result;    //Bearer
            ViewBag.expires_at = HttpContext.GetTokenAsync("expires_at").Result;    // "2021-02-01T10:58:28.0000000+00:00"

Sample code to make a request

var accessToken = await HttpContext.GetTokenAsync("access_token");

var authheader = new AuthenticationHeaderValue("Bearer", accessToken);


var client = new HttpClient();


var authheader = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Authorization = authheader;

var content = await client.GetStringAsync("https://localhost:7001/api/payment");

ViewBag.Json = JObject.Parse(content).ToString();
return View();
Tore Nestenius
  • 16,431
  • 5
  • 30
  • 40
  • Thank you, is there a specific place where I would need to implement this? – NoviceCoder Jun 22 '21 at 14:10
  • My code shows how you can get hold of the tokens. You need to do the above each time you need to call an API protected by an access token. – Tore Nestenius Jun 22 '21 at 14:11
  • Your sample code, isn't this the same thing I am doing inside of my `TokenService.cs` -> `GetToken()` method transitioning over to the `HomeController.cs` -> `Weather()` method? Check the updated section – NoviceCoder Jun 22 '21 at 14:22
  • Why do you use both the client credentials flow and the authorization code flow? when you login in your token service using the client credentials flow, there is no user involved, so calling the userinfo endpoint seems pointless? (not sure if that is even possible or not). My code is based on the token returned back using the AddOpenIDConnect. It will get its own token based on the user logged in. You typically don't need to use both for protecting a typical web based application. Client cred. flow is only used for service to service communication where no user is involved. – Tore Nestenius Jun 22 '21 at 16:35
  • Oh I see, so if I just use the authorization code flow (your code) will protect it as well and include the user? – NoviceCoder Jun 22 '21 at 17:15
  • Whenever I do `HttpContext.GetTokenAsync()` it says `HttpContext` does not contain a definition for `GetTokenAsync`... I even added the package Microsoft.aspnetcore.authorization – NoviceCoder Jun 22 '21 at 17:30
  • You don't find the method GetTokenAsync? Sample code can be found here https://github.com/DuendeSoftware/IdentityServer/blob/main/clients/src/MvcCode/Controllers/HomeController.cs – Tore Nestenius Jun 22 '21 at 18:17
  • Nope, does it matter about what kind of project it is? Because I am using a blazor project but it is a .cs file. I even tried copying and placing it in my class and same thing... – NoviceCoder Jun 22 '21 at 18:49
  • Also, `ViewBag.Json` tells me viewbag does not exist in the current context – NoviceCoder Jun 22 '21 at 19:43
  • Aha, you are in Blazor, that's can affect it all, I assumed you were running ASP.NET Core MVC. Does this help? https://stackoverflow.com/questions/59891352/how-do-i-get-the-access-token-from-a-blazor-server-side-web-app or this one? https://code-maze.com/using-access-token-with-blazor-webassembly-httpclient/ – Tore Nestenius Jun 23 '21 at 06:52
  • Makes sense now why why it wasn't working the way you had it set up, I will look into these links thank you! – NoviceCoder Jun 23 '21 at 17:17
  • 1
    One option is to do the authentication part in your backend ASP.NET core and after login you jump back to Blazor. Not everything must be made in Blazor. :-) Just to keep it simple :-) – Tore Nestenius Jun 23 '21 at 19:10
  • Muchly appreciated, definitely simplifies everything :) – NoviceCoder Jun 23 '21 at 19:55