We are using .NET Core 3.1 to develop a web application. We want to use Google.Apis.Drive.v3 NuGet package to list all files saved in Google Drive. The account from which we want to retrieve files will always be the same, ex. company-data#company.com. We found official documentation on how to authenticate in web applications. However, this example doesn't seem to be working in .NET Core.
Can anyone provide a simple example on how to authenticate against Google Drive API and list all files. Official documentation doesn't cover .NET Core at all.
EDIT: There is a very similar question here. Unfortunately, there are no answers.
This is an example with Google analytics let me know if you need help altering it for drive.
startup.cs
public class Client
{
public class Web
{
public string client_id { get; set; }
public string client_secret { get; set; }
}
public Web web { get; set; }
}
public class ClientInfo
{
public Client Client { get; set; }
private readonly IConfiguration _configuration;
public ClientInfo(IConfiguration configuration)
{
_configuration = configuration;
Client = Load();
}
private Client Load()
{
var filePath = _configuration["TEST_WEB_CLIENT_SECRET_FILENAME"];
if (string.IsNullOrEmpty(filePath))
{
throw new InvalidOperationException(
$"Please set the TEST_WEB_CLIENT_SECRET_FILENAME environment variable before running tests.");
}
if (!File.Exists(filePath))
{
throw new InvalidOperationException(
$"Please set the TEST_WEB_CLIENT_SECRET_FILENAME environment variable before running tests.");
}
var x = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject<Client>(File.ReadAllText(filePath));
}
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ClientInfo>();
services.AddControllers();
services.AddAuthentication(o =>
{
// This is for challenges to go directly to the Google OpenID Handler, so there's no
// need to add an AccountController that emits challenges for Login.
o.DefaultChallengeScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
// This is for forbids to go directly to the Google OpenID Handler, which checks if
// extra scopes are required and does automatic incremental auth.
o.DefaultForbidScheme = GoogleOpenIdConnectDefaults.AuthenticationScheme;
o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddGoogleOpenIdConnect(options =>
{
var clientInfo = new ClientInfo(Configuration);
options.ClientId = clientInfo.Client.web.client_id;
options.ClientSecret = clientInfo.Client.web.client_secret;
});
services.AddMvc();
}
// 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.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
Controller with Auth
[ApiController]
[Route("[controller]")]
public class GAAnalyticsController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public GAAnalyticsController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
// Test showing use of incremental auth.
// This attribute states that the listed scope(s) must be authorized in the handler.
[GoogleScopedAuthorize(AnalyticsReportingService.ScopeConstants.AnalyticsReadonly)]
public async Task<GetReportsResponse> Get([FromServices] IGoogleAuthProvider auth, [FromServices] ClientInfo clientInfo)
{
var GoogleAnalyticsViewId = "78110423";
var cred = await auth.GetCredentialAsync();
var service = new AnalyticsReportingService(new BaseClientService.Initializer
{
HttpClientInitializer = cred
});
var dateRange = new DateRange
{
StartDate = "2015-06-15",
EndDate = "2015-06-30"
};
// Create the Metrics object.
var sessions = new Metric
{
Expression = "ga:sessions",
Alias = "Sessions"
};
//Create the Dimensions object.
var browser = new Dimension
{
Name = "ga:browser"
};
// Create the ReportRequest object.
var reportRequest = new ReportRequest
{
ViewId = GoogleAnalyticsViewId,
DateRanges = new List<DateRange> {dateRange},
Dimensions = new List<Dimension> {browser},
Metrics = new List<Metric> {sessions}
};
var requests = new List<ReportRequest> {reportRequest};
// Create the GetReportsRequest object.
var getReport = new GetReportsRequest {ReportRequests = requests};
// Make the request.
var response = service.Reports.BatchGet(getReport).Execute();
return response;
}
}
You can use Google.Apis.Auth.AspNetCore3 for authenticating in .NET Core 3.1. The Google.Apis.Auth.AspNetCore3.IntegrationTests is a good example (it's just an ASP.NET Core 3 Web Application) of how to use the library and shows all of its features. Feel free to create issues in https://github.com/googleapis/google-api-dotnet-client if you encounter any problems.
Related
Im having some issues here with Opentracing and Jaegertracing when it comes to C#. I have had this working before, but with Java projects. So I start to wonder what Im missing when it comes to C# .NET Core web service.
This is my class to start my tracer to be used
public static class MyTracer
{
public static ITracer tracer = null;
public static ITracer InitTracer()
{
Environment.SetEnvironmentVariable("JAEGER_SERVICE_NAME", "my-store");
Environment.SetEnvironmentVariable("JAEGER_AGENT_HOST", "192.168.2.27");
Environment.SetEnvironmentVariable("JAEGER_AGENT_PORT", "6831");
Environment.SetEnvironmentVariable("JAEGER_SAMPLER_TYPE", "const");
Environment.SetEnvironmentVariable("JAEGER_REPORTER_LOG_SPANS", "false");
Environment.SetEnvironmentVariable("JAEGER_SAMPLER_PARAM","1");
Environment.SetEnvironmentVariable("JAEGER_SAMPLER_MANAGER_HOST_PORT", "5778");
Environment.SetEnvironmentVariable("JAEGER_REPORTER_FLUSH_INTERVAL" , "1000");
Environment.SetEnvironmentVariable("JAEGER_REPORTER_MAX_QUEUE_SIZE" , "100");
var loggerFactory = new LoggerFactory();
var config = Configuration.FromEnv(loggerFactory);
tracer = config.GetTracer();
if (!GlobalTracer.IsRegistered())
{
GlobalTracer.Register(tracer);
}
return tracer;
}
}
Controller code that should report to the Jaeger agent and collector for show in the UI.
[Route("api/[controller]")]
[ApiController]
public class ComponentController : ControllerBase
{
private readonly ITracer tracer;
public ComponentController(ITracer tracer)
{
this.tracer = tracer;
}
/// <summary>
/// Get component by ID
/// </summary>
/// <returns></returns>
[HttpGet("GetComponent")]
public ActionResult<ComponentModel> GetComponent(string id)
{
var builder = tracer.BuildSpan("operationName");
var span = builder.Start();
// Set some context data
span.Log("Getting data");
span.SetTag(Tags.SpanKind, "Getting data request");
span.Finish();
ComponentModel component = ComponentManager.GetComponent(id);
return component;
}
}
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Use "OpenTracing.Contrib.NetCore" to automatically generate spans for ASP.NET Core, Entity Framework Core, ...
// See https://github.com/opentracing-contrib/csharp-netcore for details.
services.AddOpenTracing();
//Init tracer
services.AddSingleton<ITracer>(t => MyTracer.InitTracer());
services.AddHealthChecks();
}
// 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.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
But this is not working at all. What am I missing here to get it work with a remote server?
Iv finally found the solution. It seemed to have to do with how the reporter is started up. Anyhow, I changed my tracer class to this.
public static class MyTracer
{
public static ITracer tracer = null;
public static ITracer InitTracer(IServiceProvider serviceProvider)
{
string serviceName = serviceProvider.GetRequiredService<IHostingEnvironment>().ApplicationName;
Environment.SetEnvironmentVariable("JAEGER_SERVICE_NAME", "my-store");
//Environment.SetEnvironmentVariable("JAEGER_AGENT_HOST", "192.168.2.27");
//Environment.SetEnvironmentVariable("JAEGER_AGENT_PORT", "6831");
//Environment.SetEnvironmentVariable("JAEGER_SAMPLER_TYPE", "const");
//Environment.SetEnvironmentVariable("JAEGER_REPORTER_LOG_SPANS", "false");
//Environment.SetEnvironmentVariable("JAEGER_SAMPLER_PARAM","1");
//Environment.SetEnvironmentVariable("JAEGER_SAMPLER_MANAGER_HOST_PORT", "5778");
//Environment.SetEnvironmentVariable("JAEGER_REPORTER_FLUSH_INTERVAL" , "1000");
//Environment.SetEnvironmentVariable("JAEGER_REPORTER_MAX_QUEUE_SIZE" , "100");
//application - server - id = server - x
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var sampler = new ConstSampler(sample: true);
var reporter = new RemoteReporter.Builder()
.WithLoggerFactory(loggerFactory)
.WithSender(new UdpSender("192.168.2.27", 6831, 0))
.Build();
tracer = new Tracer.Builder(serviceName)
.WithLoggerFactory(loggerFactory)
.WithSampler(sampler)
.WithReporter(reporter)
.Build();
if (!GlobalTracer.IsRegistered())
{
GlobalTracer.Register(tracer);
}
return tracer;
}
}
I know there are several inactive variables here right now. Will see if they still can be of use some how. But none is needed right now to get it rolling.
Hope this might help someone else trying to get the .NET Core working properly together with a remote Jeagertracing server.
I've created a new controller in a brand new web api project in .net core 3.1. Whenever I try to post to the route I get a 404.
The controller is set as so:
[ApiController]
[Route("[controller]")]
public class AuthCodeController : Controller
{
private readonly ApplicationContext _context;
public AuthCodeController(ApplicationContext context)
{
_context = context;
}
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
public JsonResult GetAuthCode(AuthCode authCode)
{
try
{
var theCodes = _context.AuthCodes.ToList();
var tmpCode = new Random();
var myNum = tmpCode.Next(100000, 999999999);
while (theCodes.Any(tc => tc.AuthCodeRnd == myNum))
{
myNum = tmpCode.Next();
}
if (authCode.AuthCodeRnd > 0)
{
var removeCode = _context.AuthCodes.FirstOrDefault(c => c.AuthCodeRnd == authCode.AuthCodeRnd);
if (removeCode != null) _context.AuthCodes.Remove(removeCode);
}
Guid authGuid = Guid.NewGuid();
var tmpRec = new AuthCode
{
Guid = authGuid,
AuthCodeRnd = myNum,
Address = authCode.tAddress,
SmallLogoAddress = authCode.SmallLogoAddress,
ClientFolder = authCode.ClientFolder,
CompanyFolder = authCode.CompanyFolder,
Folder = authCode.Folder
};
_context.AuthCodes.Add(tmpRec);
_context.SaveChanges();
var retVals = new AuthResponse
{
Guid = authGuid,
ReturnAuthCode = myNum
};
return Json(retVals);
}
catch (Exception ex)
{
var message = new ErrorResponse();
message.Status = "An Error Has Occured";
message.Message = ex.ToString();
return Json(message);
}
}
}
When I POST to this method I receive a 404. I'm using the url https://localhost:44328/AuthCode/GetAuthCode
The only modifications I made in the startup.cs are adding the dbcontext service options. Everything else is default. I can get the weatherforecast to show.
EDIT - added startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllers();
}
// 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.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Solved: I needed to disable SSL Verification in POSTMan
You should set attribute: [HttpPost] → [HttpPost("GetAuthCode")] since your original route will be simple POST to 'https://localhost:44328/AuthCode'. Core Controller does use reflection to Your Controller name 'AuthCodeController' to form prefix for Your path ('/AuthCode' part). But it does not use reflection to form postfix from function name - You should form it yourself by parameter in HttpPost attribute.
I am using client credential/app identity flow (OAuth 2.0) where the API is able to authenticate the web app by its app id. There are 2 things that I need to make sure the authentication is successful:
The access token passed from web app to access the API should be a valid bearer token (eg: not expired, valid format, etc)
The app id from the access token has to be the specified web app
When I put the [authorize] attribute in the controller class, it kept returning 401.
Here is startup.cs class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseMvc();
}
AzureAdAuthenticationBuilderExtentsions class
public static class AzureAdAuthenticationBuilderExtentsions
{
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
=> builder.AddAzureAdBearer(_ => { });
public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
builder.AddJwtBearer();
return builder;
}
private class ConfigureAzureOptions : IConfigureNamedOptions<JwtBearerOptions>
{
private readonly AzureAdOptions _azureOptions;
public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
{
_azureOptions = azureOptions.Value;
}
public void Configure(string name, JwtBearerOptions options)
{
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidAudiences = new string[] {
_azureOptions.ClientId,
_azureOptions.ClientIdUrl
},
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
RequireExpirationTime = true
};
options.Audience = _azureOptions.ClientId;
options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
}
public void Configure(JwtBearerOptions options)
{
Configure(Options.DefaultName, options);
}
}
}
Here's AzureAdOptions class
public class AzureAdOptions
{
internal static readonly object Settings;
public string ClientId { get; set; }
public string ClientIdUrl { get; set; }
public string ClientSecret { get; set; }
public string Instance { get; set; }
public string Domain { get; set; }
public string TenantId { get; set; }
}
And controller class
[Route("api")]
[ApiController]
public class FindController : ControllerBase
{
private IConfiguration _configuration;
HttpClient _client;
public ContentController( IConfiguration configuration)
{
_configuration = configuration;
}
private bool ValidateRequest()
{
var authHeader = Request.Headers["Authorization"];
if (StringValues.IsNullOrEmpty(authHeader) || authHeader.Count == 0)
{
throw new UnauthorizedAccessException(Messages.AuthHeaderIsRequired);
}
var tokenWithBearer = authHeader.Single();
var token = tokenWithBearer.Substring(7); //remove bearer in the token
var jwtHandler = new JwtSecurityTokenHandler();
if (!jwtHandler.CanReadToken(token))
{
throw new FormatException("Invalid JWT Token");
}
var tokenS = jwtHandler.ReadToken(token) as JwtSecurityToken;
var appId = tokenS.Audiences.First();
if (string.IsNullOrEmpty(appId))
{
throw new UnauthorizedAccessException(Messages.AppIdIsMissing);
}
var registeredAppId = _configuration.GetSection("AzureAd:AuthorizedApplicationIdList")?.Get<List<string>>();
return (registeredAppId.Contains(appId)) ? true : false;
}
[HttpPost("Find")]
[Produces("application/json")]
[Authorize]
public async Task<IActionResult> Find()
{
try
{
if (!ValidateRequest())
{
return Unauthorized();
}
return new ObjectResult("hello world!");
}
catch (InvalidOperationException)
{
return null;
}
}
}
Anyone knows why it keeps returning 401 error? One thing I would like to mention is between the time I start calling the API till it returns 401 error, the break points inside the controller class never got hit...
If the resource is App ID URI of the api application when acquiring access token for accessing api application . In api application , allowed audience should also include the App ID URI of the api application .
I would make 2 points for you to check:
1. Check for appID matching AuthorizedApplicationIdList in your code
I think the way you have described conditions to check in words is fine, but there is a problem with how you have implemented the second condition in code.
The app id from the access token has to be the specified web app
When implementing this condition, it seems you are setting appId as the value from aud i.e. audience claim in token. This is incorrect, because audience will always be your own API for which this token is intended.
What you want to check instead is the value for appid claim in token, which will be the application ID for client that acquired this token. This should be the front end web app's application ID which you want to check against your list of Authorized applications.
Take a look at Microsoft Docs reference for Access Tokens
Also, you can verify this easily by decoding the token using https://jwt.ms
Relevant code from your post where I see issue:
var appId = tokenS.Audiences.First();
if (string.IsNullOrEmpty(appId))
{
throw new UnauthorizedAccessException(Messages.AppIdIsMissing);
}
var registeredAppId = _configuration.GetSection("AzureAd:AuthorizedApplicationIdList")?.Get<List<string>>();
return (registeredAppId.Contains(appId)) ? true : false;
2. General Log/Debug
Also, on a side note, you can probably debug or put log/trace statements in your API code to find out exactly where it's failing in your code.. or if it unexpectedly fails somewhere even before your custom logic is called. Maybe while some of the initial validations are being performed.
I am looking for sample code and examples regarding how to implement authorization at resolver function level using GraphQL.NET and ASP.NET CORE 2.
Basically I am trying to prevent the execution of query if the request is not authorized.
Can anyone help me to get some good tutorials or code samples as reference for the implementation.
For graphql-dotnet/authorization, the page for AspNetCore has not been released, refer Add GraphQL.Server.Authorization.AspNetCore NuGet package #171.
You could implement Authorization.AspNetCore for your own use.
After implement Authorization.AspNetCore, you could configure the Authorize like:
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
{
Configuration = configuration;
Environment = hostingEnvironment;
}
public IConfiguration Configuration { get; }
public IHostingEnvironment Environment { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(option =>
{
option.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
option.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
services.AddGraphQL(options =>
{
options.EnableMetrics = true;
options.ExposeExceptions = Environment.IsDevelopment();
//options.
})
.AddGraphQLAuthorization(options =>
{
options.AddPolicy("Authorized", p => p.RequireAuthenticatedUser());
//var policy = new AuthorizationPolicyBuilder()
// .
//options.AddPolicy("Authorized", p => p.RequireClaim(ClaimTypes.Name, "Tom"));
});
//.AddUserContextBuilder(context => new GraphQLUserContext { User = context.User });
services.AddSingleton<MessageSchema>();
services.AddSingleton<MessageQuery>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseGraphQL<MessageSchema>("/graphql");
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions()
{
Path = "/ui/playground"
});
app.UseGraphiQLServer(new GraphiQLOptions
{
GraphiQLPath = "/ui/graphiql",
GraphQLEndPoint = "/graphql"
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Schema
public class MessageQuery : ObjectGraphType<Message>
{
public MessageQuery()
{
Field(o => o.Content).Resolve(o => "This is Content").AuthorizeWith("Authorized");
Field(o => o.SentAt);
Field(o => o.Sub).Resolve(o => "This is Sub");
}
}
For complete demo, refer GraphQLNet.
To get GraphQL.Net's authorization to work in ASP.NET Core, first install this package:
GraphQL.Server.Authorization.AspNetCore
In Startup.cs add the following in ConfigureServices. Make sure to add these using statements:
using GraphQL.Validation;
using GraphQL.Server.Authorization.AspNetCore;
public void ConfigureServices(IServiceCollection services)
{
//... other code
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddTransient<IValidationRule, AuthorizationValidationRule>()
.AddAuthorization(options =>
{
options.AddPolicy("LoggedIn", p => p.RequireAuthenticatedUser());
});
//... other code
}
Now you'll be able to use AuthorizeWith() at the resolver level to protect the field. Example:
public class MyQuery : ObjectGraphType
{
public MyQuery(ProductRepository productRepository)
{
Field<ListGraphType<ProductType>>(
"products",
resolve: context => productRepository.GetAllAsync()
).AuthorizeWith("LoggedIn");
}
}
You can also protect all queries by adding this.AuthorizeWith() to the top of the Query constructor like this:
public class MyQuery : ObjectGraphType
{
public MyQuery(ProductRepository productRepository)
{
this.AuthorizeWith("LoggedIn");
Field<ListGraphType<ProductType>>(
"products",
resolve: context => productRepository.GetAllAsync()
);
}
}
With that, any unauthenticated access to your GraphQL endpoint will be rejected.
Now in terms of logging someone in, there are many ways to do that. Here's a quick Cookie based authentication example:
Configure cookie based authentication in Startup.cs' ConfigureServices:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(o =>
{
o.Cookie.Name = "graph-auth";
});
Use mutation to log someone in:
public class Session
{
public bool IsLoggedIn { get; set; }
}
public class SessionType : ObjectGraphType<Session>
{
public SessionType()
{
Field(t => t.IsLoggedIn);
}
}
public class MyMutation : ObjectGraphType
{
public MyMutation(IHttpContextAccessor contextAccessor)
{
FieldAsync<SessionType>(
"sessions",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<StringGraphType>> { Name = "password" }),
resolve: async context =>
{
string password = context.GetArgument<string>("password");
// NEVER DO THIS...for illustration purpose only! Use a proper credential management system instead. :-)
if (password != "123")
return new Session { IsLoggedIn = false };
var principal = new ClaimsPrincipal(new ClaimsIdentity("Cookie"));
await contextAccessor.HttpContext.SignInAsync(principal, new AuthenticationProperties
{
ExpiresUtc = DateTime.UtcNow.AddMonths(6),
IsPersistent = true
});
return new Session { IsLoggedIn = true };
});
}
}
I am working on an Web API application that will be secured used Identity Server 4. The user is authenticated using implicit flow using a JavaScript client.
I am having problems reading the IdentityResource scopes in my Web API client. I think I might be missing something in the configuration when I invoke app.UseIdentityServerAuthentication( ) in my Startup.cs. I am not seeing the JSON data in any of these IdentityResource scopes when I examine the User.Claims collection.
In the JavaScript client, I am able to view the scope data using the oidc-client.js library. But, I cannot read the scope data in my Web API application.
In the javascript client, I can see the scope data stored as json blob by doing this in TypeScript:
class MyService {
private _userManager: Oidc.UserManager;
private _createUserManager(authority: string, origin: string) {
// https://github.com/IdentityModel/oidc-client-js/wiki#configuration
var config : Oidc.UserManagerSettings = {
authority: authority,
client_id: "myapi",
redirect_uri: `${origin}/callback.html`,
response_type: "id_token token",
scope: "openid profile user.profile user.organization ...",
post_logout_redirect_uri: `${origin}/index.html`
};
this._userManager = new Oidc.UserManager(config);
}
// snip
user() {
this._userManager.getUser().then(user => {
const userProfile = JSON.parse(user.profile["user.profile"]);
console.log(userProfile);
const userOrganization = JSON.parse(user.profile["user.organization"]);
console.log(userOrganization);
});
}
}
In my Web API Project, I have:
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public IContainer Container { get; set; }
public Startup(IHostingEnvironment env)
{
var configurationBuilder = new ConfigurationBuilder();
if (env.IsDevelopment())
{
configurationBuilder
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json");
env.ConfigureNLog("nlog.development.config");
}
else
{
configurationBuilder.AddEnvironmentVariables();
env.ConfigureNLog("nlog.config");
}
Configuration = configurationBuilder.Build();
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services
.AddMvcCore()
.AddJsonFormatters()
.AddAuthorization();
services.AddCors();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IRestService, RestService>();
var builder = AutoFacConfig.Create(Configuration);
builder.Populate(services);
Container = builder.Build();
return new AutofacServiceProvider(Container);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseCors(policy =>
{
policy
.WithOrigins(
"http://app.local:5000"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
ConfigureExceptionHandling(app, env);
ConfigureOidc(app);
app.UseMvc();
}
private void ConfigureExceptionHandling(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
}
private void ConfigureOidc(IApplicationBuilder app)
{
app.UseCors("default");
app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{
Authority = Configuration["IdentityServerUrl"],
AllowedScopes = { "api1", "openid", "profile", "user.profile", "user.organization" ... },
ApiName = "myapi",
RequireHttpsMetadata = false
});
}
}
The problem is when I try to access the claims to read its scopes. When the user is authenticated, I pass the token in the header of the request to the Web API endpoint. I have a BaseController in which I would like to read the scopes from, but cannot.
public class BaseController : Controller
{
private ApplicationUser _user;
public string UserId => CurrentUser?.UserInfo.Id;
public string OrganizationId => CurrentUser?.Organization.Id;
[CanBeNull]
public ApplicationUser CurrentUser
{
get
{
var claims = Request.HttpContext.User.Claims.ToList();
_user = new ApplicationUser
{
UserInfo = claims.Where(f => f.Type == "user.profile").Select(s => s.Value).FirstOrDefault(),
OrganizationInfo = claims.Where(f => f.Type == "user.organization").Select(s => s.Value).FirstOrDefault()
};
return _user;
}
}
}
If, I look at the claims collection, I see the following for Types and values:
nbf: 1499441027
exp: 1499444627
iss: https://identity-service....
aud: https://identity-service..../resources
aud: myapi
client_id: myapi
sub: the_user
auth_time: 1499441027
idp: local
scope: openid
scope: profile
scope: user.profile
scope: user.organization
...
scope: myapi
amr: pwd
How do I read scope data from C#?
For user.organization scope, I am expecting to read a data structure like:
{
"Id":"some id",
"BusinessCategory":"some category",
"Name":"some name"
}
Scope is Type and Profile is Value for one of the scopes, that is why it is like that. What value you want to see for profile?? It doesn't have any value as it is a value itself. You are asking for it:
AllowedScopes = { "api1", "openid", "profile", "user.profile", ... },
and that is what you are getting in your scope in the claims list.
If you want to fetch a particular value from the claims list, you can also use JwtRegisteredClaimNames using
User.FindFirst(JwtRegisteredClaimNames.Email).Value
In case this is useful to anyone. I have implemented a hack. I added extra code in my middleware to call the identity server's user info endpoint. The user info endpoint will return the data that I need. I then manually add claims with this data.
I have added this code to my Configure method:
app.Use(async (context, next) =>
{
if (context.User.Identity.IsAuthenticated)
{
using (var scope = Container.BeginLifetimeScope())
{
// fetch resource identity scopes from userinfo endpoint
var restService = scope.Resolve<IRestService>();
var token = context.Request.Headers["Authorization"]
.ToString().Replace("Bearer ", string.Empty);
restService.SetAuthorizationHeader("Bearer", token);
// fetch data from my custom RestService class and
// serialize into my custom ScopeData object
var data = await restService.Get<ScopeData>(
Configuration["IdentityServerUrl"], "connect/userinfo");
if (data.IsSuccessStatusCode)
{
// add identity resource scopes to claims
var claims = new List<Claim>
{
new Claim("user.organization", data.Result.Organization),
new Claim("user.profile", data.Result.UserInfo)
};
}
else
{
sLogger.Warn("Failed to retrieve identity scopes from profile service.");
context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
}
}
}
await next();
});