asp.net-core-mvcasp.net-identityidentityserver4openid-connectuserinfo

Retrieving access token for UserInfo


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


Solution

  • 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();