graphql-dotnet / authorization Goto Github PK
View Code? Open in Web Editor NEWA toolset for authorizing access to graph types for GraphQL .NET.
License: MIT License
A toolset for authorizing access to graph types for GraphQL .NET.
License: MIT License
I've tried to follow the given example code, but I'm not seeing anything happening in my api.
Is there a full example available?
If you could get a very basic one, I'd be more than willing to enhance it.
I have created the API solution on .Net Core 3.1 with graphql, I am trying to authenticate the graphql endpoint, kindly help me to figure out where I am doing wrong..
Startup.cs code
public class Startup
{
private const string _connectionStringName = "DefaultConnection";
public IConfiguration Configuration { get; }
private readonly IWebHostEnvironment _env;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
_env = env;
}
protected virtual void ConfigureDatabase(DbContextOptionsBuilder ctxBuilder)
{
ctxBuilder.UseSqlServer(Configuration.GetConnectionString(_connectionStringName));
}
protected virtual void AddDbContext(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(ConfigureDatabase);
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.Configure<DatabaseConfiguration>(Configuration.GetSection("ConnectionStrings"));
services.AddIdentity<ApplicationUser, ApplicationRole>() //ApplicationRole
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options =>
{
// Password settings
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = false;
options.Password.RequiredUniqueChars = 6;
// Lockout settings
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.Lockout.AllowedForNewUsers = true;
// User settings
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
});
services.Configure<MongoSettings>(Configuration.GetSection("MongoDbSettings"));
services.Configure<JwtOptions>(Configuration.GetSection("JwtToken"));
services.Configure<ApplicationUrl>(Configuration.GetSection("ApplicationUrl"));
services.AddScoped<IConnectionFactory, ConnectionFactory>();
services.AddScoped<IEmailRepository, EmailRepository>();
services.AddScoped<ITokenGenerator, TokenGenerator>();
services.AddScoped<IAccountRepository, AccountRepository>();
services.AddGraphQL(o =>
{
o.ExposeExceptions = _env.EnvironmentName == "Development";
})
.AddGraphTypes(ServiceLifetime.Scoped)
.AddDataLoader()
.AddWebSockets();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services
.AddTransient<IValidationRule, AuthorizationValidationRule>()
.AddAuthorization(options =>
{
options.AddPolicy(Policies.Admin, Policies.AdminPolicy());
options.AddPolicy(Policies.User, Policies.UserPolicy());
});
var JwtSection = Configuration.GetSection("JwtToken");
var JwtOptions = new JwtOptions();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = JwtOptions.Issuer,
ValidAudience = JwtOptions.Issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration[JwtOptions.SecretKey])),
ClockSkew = TimeSpan.Zero
};
});
services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<AppSchema>();
// If using Kestrel:
services.Configure<KestrelServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
// If using IIS:
services.Configure<IISServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
}
// 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();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseGraphQLWebSockets<AppSchema>("/graphql");
app.UseGraphQL<AppSchema>();
app.UseGraphQLPlayground(options: new GraphQLPlaygroundOptions());
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
**GraphQL Query**
public AppQuery(IAccountRepository accountRepository)
{
FieldAsync<ListGraphType<UserMasterType>>("getUserMaster",
arguments: new QueryArguments(new List<QueryArgument>
{
new QueryArgument<IntGraphType>
{
Name = "id"
},
new QueryArgument<StringGraphType>
{
Name = "email"
},
new QueryArgument<StringGraphType>
{
Name = "userId"
},
new QueryArgument<IntGraphType>
{
Name = "userType"
},
}),
resolve: async context =>
{
var Id = context.GetArgument<int>("id");
var Email = context.GetArgument<string>("email");
var UserId = context.GetArgument<string>("userId");
var UserType = context.GetArgument<int>("userType");
return await accountRepository.GetUserMaster(Id, UserId, Email, UserType);
}
);
FieldAsync<ListGraphType<RoleMasterType>>("getUserRoles",
arguments: new QueryArguments(new List<QueryArgument>
{
new QueryArgument<IntGraphType>
{
Name = "roleType"
},
}),
resolve: async context =>
{
var RoleType = context.GetArgument<int>("roleType");
return await accountRepository.GetUserRoles(RoleType);
}
).AuthorizeWith(Policies.User);
}
**Code to generate JWT Token**
var tokenResponse = new TokenResponse();
var claims = new List<Claim>
{
new Claim(ClaimTypes.Email, Email),
new Claim(ClaimTypes.Sid, Guid.NewGuid().ToString()),
new Claim("role","User")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expires = DateTime.Now.AddMinutes(Convert.ToDouble(_options.ExpiryMinutes));
var token = new JwtSecurityToken(
_options.Issuer,
_options.Issuer,
claims,
expires: expires,
signingCredentials: creds
);
var AccessToken = new JwtSecurityTokenHandler().WriteToken(token);
Getting the following error
Hey guys,
so I have graphql as backend and React / Apollo as Frontend.
I have allready implemented my JWT Token Auth, which works fine.
Additional to that I have my Middleware, in which the HttpContext is given and the user ist correctly loaded with all Claims:
namespace xxx.Web.GQL.Middleware
{
public class GraphQLMiddleware
{
private readonly RequestDelegate _next;
private readonly IDocumentWriter _writer;
private readonly IDocumentExecuter _executor;
private readonly ISchema _schema;
public GraphQLMiddleware(RequestDelegate next, IDocumentWriter writer, IDocumentExecuter executor, ISchema schema)
{
_next = next;
_writer = writer;
_executor = executor;
_schema = schema;
}
public async Task InvokeAsync(HttpContext httpContext)
{
if (httpContext.Request.Path.StartsWithSegments("/graphql") && string.Equals(httpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase))
{
string body;
using (var streamReader = new StreamReader(httpContext.Request.Body))
{
body = await streamReader.ReadToEndAsync();
var request = JsonConvert.DeserializeObject<GraphQLQuery>(body);
var result = await _executor.ExecuteAsync(doc =>
{
doc.Schema = _schema;
doc.Query = request.Query;
doc.Inputs = request.Variables.ToInputs();
doc.ExposeExceptions = true;
doc.UserContext = httpContext.User;
}).ConfigureAwait(false);
var json = _writer.Write(result);
await httpContext.Response.WriteAsync(json);
}
}
else
{
await _next(httpContext);
}
}
}
}
Until here it works perfectly fine.
Sadly I am strugling with any futher. I added the GraphQL.Authorization Nuget but all given information are not enough that I could build some working code with it.
What I could do is ofc access the userContext within the resolver of a query and check it "by hand" but I try to avoid it ;)
Field<StringGraphType>(
name: "hallo",
resolve: c =>
{
var userPrinc = (ClaimsPrincipal)c.UserContext;
var allowed = userPrinc.Claims.Any(x => x.Type == "Role" && x.Value == "Admin" || x.Value == "Mod");
if (!allowed)
{
throw new Exception("TODO: Make this a 401 FORBIDDEN");
}
return "World";
}
So what I want is:
Check the claims on Field-Level (for Query or Mutation) for a given Claim with one or more roles in it.
Hi, as I need one registered service to evaluate the authorization, is there a way to use Dependency Injection when implementing IAuthorizationRequirement
?
I want to achieve something similar to handlers in in ASP.NET Core.
Thanks!
The first thing in the default AuthorizationEvaluator .Evaluate method should be a check of principal.Identity.IsAuthenticated to conditionally return an AuthenticationResult.Fail() if false since claims are only valid for an authenticated user.
Hello!
I was searching for examples but couldn't find anything that shows how to create a JWT token that can hold the user's roles. I see some examples like this:
authSettings.AddPolicy("AdminPolicy", _ => _.RequireClaim("role", "Admin"))
Anyway I can't understand how to add that claim into my JWT token. Before I started using GraphQL I was generationg my JWTs with this code:
public string CreateUserToken(User user, Guid companyId)
{
DateTime nowUtc = DateTime.UtcNow;
DateTime expires = nowUtc.AddMinutes(_options.UserTokenLifetime);
DateTime centuryBegin = new DateTime(1970, 1, 1).ToUniversalTime();
long exp = (long)(new TimeSpan(expires.Ticks - centuryBegin.Ticks).TotalSeconds);
long now = (long)(new TimeSpan(nowUtc.Ticks - centuryBegin.Ticks).TotalSeconds);
var payload = new JwtPayload
{
{ "sub", user.Id },
{ "iss", _options.Issuer },
{ "iat", now },
{ "exp", exp },
{ "company", companyId },
{ "roles", JsonConvert.SerializeObject(user.GetRolesByCompanyId(companyId)) }
};
return _jwtSecurityTokenHandler.WriteToken(new JwtSecurityToken(_jwtHeader, payload));
}
The roles array was automatically recognized by dotnet core and I was able to use the authorization attributes on my controllers. However in graphql this is not supported or I am making a mistake.
I would be grateful if anyone could help me resolve the issue.
I have a use case where I want a subset of my queries and mutations to require authentication with a jwt token and another subset that requires authentication.
I have set up the JWT authentication in startup like this
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "JwtTokens";
options.DefaultChallengeScheme = "JwtTokens";
})
.AddJwtBearer("JwtTokens", jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
IssuerSigningKeys = new List<RsaSecurityKey>
{
AuthUtils.GetSigningKey(Settings.PublicKey)
},
ValidIssuers = new List<string>{
Settings.AuthIssuer,
},
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = false,
RoleClaimType = "groups",
ClockSkew = TimeSpan.FromMinutes(5)
};
});
I then need to put the [Authorize]
attribute on my endpoint to read the JWT token, but then I wont get access to the endpoints that I want to be available without authentication.
[HttpPost]
[Authorize]
public Task<IActionResult> Post([FromBody] GraphQLParameter query)
{
if (query == null) { throw new ArgumentNullException(nameof(query)); }
return PostInternal(query);
}
Any suggestions on how I can solve this issue?
I am struggling to figure out how to inject into an AuthorizationRequirement
I am trying to inject IClient in the following code example.
When setting up the policy, I have to use a constructor which means I am not using DI
_.AddPolicy(GraphQLPolicyNames.HomeAccess, policy =>
{
policy.AddRequirement(new BasicAccessRequirement());
policy.AddRequirement(new HomeAccessRequirement());
});
public class HomeAccessRequirement : IAuthorizationRequirement
{
private readonly IClient client;
public HomeAccessRequirement(IClient client)
{
this.client = client;
}
public Task Authorize(AuthorizationContext context)
{
if (AuthorizationHelpers.IsAdminOrSuperAdmin(context.User))
{
return Task.FromResult(0);
}
var hasId = context.InputVariables.ContainsKey("Id");
if (hasId)
{
dynamic id;
context.InputVariables.TryGetValue("Id", out id);
var home = client.GetData(id);
}
return Task.FromResult(0);
}
}
How can I achieve DI to inject the IClient which is needed for authorization
I have been at it for almost 2 days trying to finalize my use of GraphQL.Authorization
to restrict access to queries and mutations intended for "Admin" users. I am using JWT for authentication, and that seems to work. I created a logonUser
mutation that returns a token that I subsequently provide in the headers section of the playground like so:
{
"Authorization":"Bearer [long token string]"
}
This is where things fall apart. I have added the following code to ConfigureServices()
:
services.AddEntityFrameworkNpgsql()
.AddDbContext<ApplicationDbContext>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<IUnitOfWork, ApplicationUnitOfWork>();
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IRoleRepository, RoleRepository>();
services.AddScoped<IBurstIqService, BurstIqService>();
services.AddScoped<IUserService, UserService>();
// TODO: Investigate repercussions of this. Does this only make execution serial
// within a scope or the whole app? Could be a bottleneck going forward.
services.AddSingleton<IDocumentExecuter, AirDocumentExecutor>();
services.AddScoped<AppSchema>();
services.AddScoped<ISchema, AppSchema>();
services.AddScoped<AppMutation>();
services.AddScoped<AppQuery>();
services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// ===== Add Jwt Authentication ========
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
services.AddAuthorization();
services.AddGraphQL(o =>
{
o.ExposeExceptions = true;
o.EnableMetrics = true;
})
.AddUserContextBuilder(context => new GraphQLUserContext { User = context.User })
.AddGraphTypes(ServiceLifetime.Scoped)
.AddDataLoader();
services.AddGraphQLAuth((_, s) =>
{
_.AddPolicy("AdminPolicy", p => p.RequireClaim("roles", "Admin"));
});
My GraphQLAuthExtensions
class looks like this:
public static class GraphQLAuthExtensions
{
public static void AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings, IServiceProvider> configure)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.TryAddTransient(s =>
{
var authSettings = new AuthorizationSettings();
configure(authSettings, s);
return authSettings;
});
}
public static void AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings> configure)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.TryAddTransient(s =>
{
var authSettings = new AuthorizationSettings();
configure(authSettings);
return authSettings;
});
}
}
When I enable the following code, the call ALWAYS succeeds no matter if I am logged in or not:
app.UseMiddleware<GraphQLMiddleware>(new GraphQLSettings
{
BuildUserContext = ctx => new GraphQLUserContext
{
User = ctx.User
},
EnableMetrics = Configuration.GetValue<bool>("EnableMetrics"),
ExposeExceptions = Configuration.GetValue<bool>("ExposeExceptions")
});
This uses the same GraphQLMiddleware
code in the harness. When I comment the above section out, I always get an error that the user does not have the right roles even though I have passed the appropriate header with a valid token.
I have tried every combination of sample code available--and followed ALL documentation, and I have been unable to get this to work. Roles and users are never detected properly, and the following code for my query doesn't work as intended:
public class AppQuery : ObjectGraphType
{
public AppQuery(
IUserService userService,
IRoleRepository roleRepository
)
{
Field<ListGraphType<UserType>>("users", resolve: context => userService.Get()).AuthorizeWith("AdminPolicy");
Field<ListGraphType<RoleType>>("roles", resolve: context => roleRepository.Get());
}
}
I have also tried various values for p.RequireClaim
call listed above. I have tried "role"
, "Role"
, and "roles"
, thinking this value might be appended to the URL string definition for a role claim.
Can anyone help me get this working? What am I missing?
None of the samples on the web seem to help at all...
I'm trying to authorize a field by validating it´s input arguments in a custom policy. My policy gets executed correctly but the Arguments property of the AuthorizationContext variable is always empty. What do I need to do in order to get this populated?
Field<DeviceContextType>(
name: "deviceContext",
description: "Get device context data by id.",
arguments: new QueryArguments(
new QueryArgument<IdGraphType> {Name = "id"},
new QueryArgument<IdGraphType> {Name = "organizationId"}
),
resolve: context => new DeviceContext
{
// my dummy object here
}
).AuthorizeWith<DeviceQueryPolicy>();
public class DeviceQueryPolicy : IAuthorizationPolicy
{
public DeviceQueryPolicy(IDependencyResolver resolver)
{
Requirements = new[]
{
new DeviceIdMatchOrganizationIdRequirement(resolver),
};
}
public IEnumerable<IAuthorizationRequirement> Requirements { get; }
}
public async Task Authorize(AuthorizationContext context)
{
// Arguments is always empty here! Why?
if (!context.Arguments.ContainsKey("id")
|| !context.Arguments.ContainsKey("organizationId")) {
context.ReportError("Organization have no access rights to this device");
return;
}
# Passing along two arguments
{
devices {
deviceContext(
id: "26748243-9D9D-4E9A-A890-718A46D2C0D5"
organizationId: "26748243-9D9D-4E9A-A890-718A46D2C0D5"
) {
active
}
}
}
I have the situation, that I have Users that should get a smaller Number of items and others should get the full List. Currently I added two Fields and protect one with a .AuthorizeWith
Field<ListGraphType<MeasurementType>>(
"allmeasurements",
resolve: context =>
{
var dataLoader = dataLoaderContextAccessor.Context.GetOrAddCollectionBatchLoader<string, Measurement>("GetAllMeasurementByDeviceId", GetAllMeasurementByDeviceId);
return dataLoader.LoadAsync(context.Source.Id);
}
).AuthorizeWith("UserPolicy");
Field<ListGraphType<MeasurementType>>(
"lastmeasurements",
resolve: context =>
{
var dataLoader = dataLoaderContextAccessor.Context.GetOrAddCollectionBatchLoader<string, Measurement>("GetLastMeasurementByDeviceId", GetLastMeasurementByDeviceId);
return dataLoader.LoadAsync(context.Source.Id);
}
);
Is there an way to do it better in grapql-dotnet?
I'm having some issues with auth between input and output types. With the following types I only got an error if I tried to use the name field in a query, but not in my mutation.
I'm using GraphQL.Authorization 2.0.27 and it seems like this should have been resolved in #17 ?
public class UserType : ObjectGraphType<User>, IGraphQLType
{
public UserType()
{
Name = "User";
Field(c => c.Id, type: typeof(IdGraphType)).Description("The id of the user");
Field(c => c.Name).Description("The name of the user").AuthorizeWith(AuthPolicies.SuperAdminPolicy.Name);
}
}
public class UserInputType : InputObjectGraphType<User>, IGraphQLType
{
public UserInputType()
{
Name = "User";
Field(c => c.Id, type: typeof(IdGraphType)).Description("The id of the user");
Field(c => c.Name).Description("The name of the user").AuthorizeWith(AuthPolicies.SuperAdminPolicy.Name);
}
}
I'm working in .Net Framework without Identity.
I'm calling "HttpContext.Current" from my "IAuthorizationRequirement" class, which works, but is a bad practice/pattern to follow.
The request would be to include in HttpContext "(AuthorizationContext context)" passed down through from the originally Web API context.
I am working on the qraphQL api and I am trying to use authorization attribute GraphQLAuthorize in GraphQL.Server.Authorization.AspNetCore; I have created policy in startup.cd configureService but not recognized by GraphQLAuthorize attribute.
The [GraphQLAuthorize(Policy = "AUTHORIZED")] is not working. while the AuthorizeWith("AUTHORIZED") is working
using GraphQL.Repository.GraphQL.Types;
using GraphQL.Repository.Repositories;
using GraphQL.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using GraphQL.Server.Authorization.AspNetCore;
namespace GraphQL.Repository.GraphQL
{
[GraphQLAuthorize(Policy = "AUTHORIZED")]
public class MenuQuery : ObjectGraphType
{
public MenuQuery(MenuRepository menuRepository)
{
Field<ListGraphType<NavigationMenuType>>(
"NavigationMenu",
arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "applicationId" }),
resolve: context =>
{
var user = (ClaimsPrincipal)context.UserContext;
var applicationId = context.GetArgument<int>("applicationId");
return menuRepository.GetNavigationMenus(applicationId);
});
}
}
}
//startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//using GraphQL;
using GraphQL.Server;
using Microsoft.EntityFrameworkCore;
using GraphQL.Repository.Entities;
using GraphQL.Repository.Repositories;
using GraphQL.Repository.GraphQL;
using GraphQL.Repository.GraphQL.Types;
using Microsoft.Extensions.DependencyInjection.Extensions;
using GraphQL.Server.Ui.GraphiQL;
using GraphQL.Server.Ui.Playground;
namespace GraphQL.Api
{
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<MenuManagementDevContext>(options =>
options.UseSqlServer(Configuration["ConnectionStrings:CarvedRockContext"]));
services.AddScoped<MenuRepository>();
services.AddScoped<MenuQuery>();
services.AddScoped<NavigationMenuType>();
services.AddScoped<RoleNavigationMenuType>();
services.AddScoped<RoleType>();
services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService));
services.AddScoped<MenuSchema>();
services.AddGraphQL(o => { o.ExposeExceptions = true; })
.AddGraphQLAuthorization(options =>
{
options.AddPolicy("AUTHORIZED", p => p.RequireAuthenticatedUser());
})
.AddGraphTypes(ServiceLifetime.Scoped).AddUserContextBuilder(httpContext => httpContext.User)
.AddDataLoader()
.AddWebSockets();
services.AddCors();
}
// 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();
}
app.UseCors(builder =>
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
app.UseWebSockets();
app.UseGraphQLWebSockets<MenuSchema>("/graphql");
app.UseGraphQL<MenuSchema>();
app.UseGraphiQLServer(new GraphiQLOptions());
app.UseGraphQLPlayground(new GraphQLPlaygroundOptions());
}
}
}
Any help would be appreciated!
I need pagination, so I used Relay to implement. But how do I use AuthorizeWith on Connection Field ?
Hello guys!
I am trying to use JWT authentication and now I need to get some info out of my token from inside my mutation. I can use IHttpContextAccesor, get the token from the request header and manually take out what I need. This is kinda not cool so is there a way to access the user's claims from a mutation?
My mutation code:
private async Task<MyModel> Resolve(ResolveFieldContext<object> context)
{
context.UserContext.Claims ???
}
That UserContext seems empty.
I tried generating my token in 2 ways:
1st way:
public string CreateUserToken(User user, Guid companyId)
{
DateTime nowUtc = DateTime.UtcNow;
DateTime expires = nowUtc.AddMinutes(_options.UserTokenLifetime);
DateTime centuryBegin = new DateTime(1970, 1, 1).ToUniversalTime();
long exp = (long)(new TimeSpan(expires.Ticks - centuryBegin.Ticks).TotalSeconds);
long now = (long)(new TimeSpan(nowUtc.Ticks - centuryBegin.Ticks).TotalSeconds);
var payload = new JwtPayload
{
{ "sub", user.Id },
{ "iss", _options.Issuer },
{ "iat", now },
{ "exp", exp },
{ "company", companyId },
{ "roles", JsonConvert.SerializeObject(user.GetRolesByCompanyId(companyId)) }
};
return _jwtSecurityTokenHandler.WriteToken(new JwtSecurityToken(_jwtHeader, payload));
}
2nd way:
public string CreateUserToken(User user, Guid companyId)
{
var claims = new List<Claim>
{
new Claim("user", user.Id.ToString()),
new Claim("company", companyId.ToString())
};
foreach (var role in user.GetRolesByCompanyId(companyId))
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.Now.AddMinutes(Convert.ToInt32(_options.UserTokenLifetime)),
Issuer = _options.Issuer ,
IssuedAt = DateTime.Now
};
return _jwtSecurityTokenHandler.WriteToken(_jwtSecurityTokenHandler.CreateToken(tokenDescriptor));
}
I followed this example project https://github.com/graphql-dotnet/authorization/issues/10 on how to enable authorization.
At this point I don't care about policies and permissions, I just need that "comapnyId" out of my token. I would be grateful if someone could help. Thanks in advance!
Should AuthorizeWith
report an error if a field the user isn't authorized for is excluded from the query via the @include
or @skip
directives? (It currently does.)
How to secure add, update and delete operations ?
In the AuthorizationMetadataExtensions class, the IProvideMetadata AuthorizeWith extension allows a list of policies; however, the corresponding FieldBuilder AuthorizeWith extension method only allows for a single policy. I believe these should behave identically, each allowing for multiple policies (which all must be true).
I suggest changing:
public static FieldBuilder<TSourceType, TReturnType> AuthorizeWith<TSourceType, TReturnType>(
this FieldBuilder<TSourceType, TReturnType> builder, string policy)
{
builder.FieldType.AuthorizeWith(policy);
return builder;
}
to
public static FieldBuilder<TSourceType, TReturnType> AuthorizeWith<TSourceType, TReturnType>(
this FieldBuilder<TSourceType, TReturnType> builder, params string[] policies)
{
builder.FieldType.AuthorizeWith(policies);
return builder;
}
Are there any plans for releasing on a regular basis?
When can we expect a new release ?
Currently, if being invoked on an input document with multiple named queries, auth validation walks the entire document. This leads to the request being rejected if any of the queries in the document fail authorization even if the failing queries are not requested.
The entire document will be traversed here: Line 59 in DocumentValidator.cs
This should be changed to only walk the selected query.
I can see the error 400 for "message": "You are not authorized to run this query. Required claim 'role' with any value of 'member' is not present.".
But I need to return 401 http status code, What can I do?
public class MyType : ObjectGraphType
{
public MyType()
{
this.AuthorizeWith("AdminPolicy");
Field<StringGraphType>("name");
}
}
I have put Authorization on my Root Query, but now "ui playground" cannot get the initial introspection query to work.
I can put the bearer token in UiPlayground to make actual queries, but the documentation on the right doesn't work
is there a way i can allow this to work?
type not found: GraphQLMiddleware. Also not in the Harness anywhere.
GraphQLSettings defines BuildUserContext with a Task, but the readme code tries to use it as a Task. I fixed by redefining BuildUserContext to use a Task.
Code in Harness looks nothing like Readme code.
Great library. Would love to know how to use it.
The following code looked a lot like ASP.NET Core to me but to my surprise it lived in the GraphQL namespace:
var authorizationSettings = new AuthorizationSettings();
authorizationSettings.AddPolicy(
"AdminPolicy",
x => x.RequireClaim("role", "admin"));
return authorizationSettings;
The AuthorizationPolicyBuilder
in ASP.NET Core has methods for RequireAuthenticatedUser
and RequireAssertion
which can be very useful. I was considering submitting a PR for these perhaps this project should just be using Microsoft.AspNetCore.Authorization
in the first place?
Thoughts @joemcbride?
Hi,
I have some authorizations setup to handle Queries and Mutations like so (simplified)
services.AddSingleton(x =>
{
AuthorizationSettings settings = new AuthorizationSettings();
settings.AddPolicy(AuthConstants.USERS_POLICY, p => p.RequireClaim(ClaimTypes.Role));
settings.AddPolicy(AuthConstants.ADMIN_POLICY, p => p.RequireClaim(ClaimTypes.Role, ((int)UserRoles.Administrator).ToString()));
settings.AddPolicy(AuthConstants.SUPERVISOR_POLICY, p => p.RequireClaim(ClaimTypes.Role, ((int)UserRoles.Administrator).ToString(),
((int)UserRoles.Supervisor).ToString()));
return settings;
})
Now I'm attempting to add Subscriptions, but it looks like the Authorizations are not working. There didn't seem to be any built-in support for authorizing Subscriptions with JWTs, so I used this class for guidance. I can successfully retrieve the token from the connection, validate it, and add it to the HTTP context in an IOperationMessageListener::BeforeHandleAsync
public Task BeforeHandleAsync(MessageHandlingContext context)
{
if (context.Message.Type == MessageType.GQL_CONNECTION_INIT)
{
JObject payload = context.Message.Payload as JObject;
if (payload.TryGetValue("Authorization", System.StringComparison.OrdinalIgnoreCase, out JToken authValue))
{
string token = authValue.Value<string>();
if (string.IsNullOrWhiteSpace(token) == false)
{
int start = token.IndexOf(BEARER, System.StringComparison.OrdinalIgnoreCase);
if (start >= 0)
{
token = token.Substring(start + BEARER_LENGTH);
_httpContextAccessor.HttpContext.User = JwtHelper.CreatePrincipal(token);
}
}
}
}
ClaimsPrincipal user = _httpContextAccessor.HttpContext.User;
context.Properties["user"] = user;
return Task.CompletedTask;
}
But the subscription endpoint still says that I'm unauthorized when I use AuthorizeWith
. Is this a bug or how can I authorize Subscriptions using JWTs? Any guidance would be much appreciated
Hello,
I would like to know how can one combine JWT and the authorization toolset. The scenario is I would like to provide an endpoint to get a JWT token and then use that token during the graph operations. Please educate me on how to go about it.
Hello there
How can i authorize both my graph input types and graph object types to only proceed if the request header contains specific header key?
Could you please provide example.
Thank you
Ashish
I only want GraphiQL/introspection look-ups to work if the application is running from localhost - otherwise I want to be able to add an "AuthorizeWith" specifically to that operation.
Otherwise I'd still want everything below that to follow the appropriate policies.
The trouble I'm running it to is the only way I can see to-do that is to stick a policy directly on the "query" graph type, but then it breaks the security free operations.
e.g. I want :
query .. my schema - blocked if not logged in (following policy)
query .. my security free table - allowed.
I have all the code needed/working - it's just this niche case I'm stuck on.
I'm hoping I just missed something and there is simple workaround for this.
Thanks!!!
I have used GraphQL.Server.Transports.AspNetCore v3.5.
In which there is change in AddUserContextBuilder()
.
Now TUserContext will be IDictionary<string, object>
instead of IProvideClaimsPrincipal.
So here need to update AuthorizationValidationRule for aligning it with the Server package.
Here is its Ref: graphql-dotnet/server#218
Hi, I am currently using a ClaimsTransformer class which inherits from IClaimsTransformation and injects custom claims (users/roles) from my db based on the user name i receive from the jwt token. What is the best way to use these custom claims to build a policy?
Thanks!
3.0.39-preview-39 is targeting 3.0.0-preview-1352 instead of 3.0.0-preview-1648.
I'm not able to binding redirect without a public token - is there another work around?
GraphQL.Authorization.AuthorizationValidationRule
throws the following exception:
Method 'ValidateAsync' in type 'GraphQL.Authorization.AuthorizationValidationRule' from assembly 'GraphQL.Authorization, Version=3.0.38.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
Libs:
GraphQL 3.0.0-preview-1352
GraphQL.Authorization 3.0.38-preview-38
I think the reason is that it tries to implement IValidationRule which comes as follows:
namespace GraphQL.Validation
{
public interface IValidationRule
{
Task<INodeVisitor> ValidateAsync(ValidationContext context);
}
}
Hello!
I see that I can define auth policies like this:
authSettings.AddPolicy("AdministratorPolicy", _ => _.RequireClaim(ClaimTypes.Role, "Administrator"));
Then I could use this like:
public class MyType : ObjectGraphType
{
public MyType()
{
this.AuthorizeWith("AdministratorPolicy");
Field<StringGraphType>("name").AuthorizeWith("SomePolicy");
}
}
So far so good, but I have parts in my app that require the user to be authenticated but do not require a specific role. Is there anyway I could define a policy that just checks if the user is authorized without specifying required roles?
Something like the [Authorize] attribute in the controllers.
Thanks in advance!
For context I have written my own IAuthorizationRequirement
. In my case, I have a type that is registered in my schema (MyType
) protected with this.AuthorizeWith("MyRegisteredPolicyName")
and authorization is working as expected for a query similar to below:
Field<MyType>(
"myType",
arguments: new QueryArguments(new QueryArgument<StringGraphType> { Name = "id" }),
resolve: context =>
{
var id = context.GetArgument<string>("id");
var obj = db.Find(id);
return obj;
});
However, when the Query resolves to a list of the same type (ListGraphType<MyType>
)
Field<ListGraphType<MyType>>(
"myTypes",
resolve: context =>
{
var list = db.All();
return list;
});
The authorization requirement is not enforced. I was expecting that whenever the type was accessed (directly or indirectly), that the authorization requirements would be enforced. Is this by design?
Can we add a check to validate it?
if (authPolicies.Count == 0)
{
return AuthorizationResult.Fail(new List() { "Unauthorized! " }); //Or some relevant message?
}
I see the harness makes use of graphql-server. The project I am working on does not, it is a .net 4.6.2 app which implements a graphql controller. Graphql itself works, but I'm a little lost how to set up authorization like this.
I've registered all the services needed, but how do I tell my executor to use the AuthorizationSettings?
{
services.AddScoped<IDocumentExecuter, DocumentExecuter>();
services.AddScoped<Query>();
foreach (var type in GetGraphQlTypes())
{
services.AddSingleton(type);
}
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.AddTransient<ACLAuthorizationRequirement>();
services.TryAddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
authSettings.AddPolicy("ACL_TenantPolicy", _ => _.AddRequirement(s.GetService<ACLAuthorizationRequirement>()));
return authSettings;
});
services.AddScoped<ISchema>(sp => new GraphQLSchema(new FuncDependencyResolver(type => sp.GetService(type))));
EfGraphQLConventions.RegisterInContainer(services, services.BuildServiceProvider().GetService<CommonLobStoreCoreContext>());
}
It seems authSettings
is never used.
FWIW, this is what my controller's method looks like:
public async Task<IActionResult> Post([FromBody] GraphQLQueryObject query, [FromServices] DataContext dataContext)
{
if (query == null) { throw new ArgumentNullException(nameof(query)); }
var inputs = query.Variables.ToInputs();
var executionOptions = new ExecutionOptions
{
Schema = _schema,
Query = query.Query,
Inputs = inputs,
UserContext = new GraphQLUserContext
{
DataContext = dataContext,
User = this.User,
},
#if (DEBUG)
ExposeExceptions = true,
EnableMetrics = true,
#endif
};
var result = await _documentExecuter.ExecuteAsync(executionOptions).ConfigureAwait(false);```
In the current version only "ReportError" from "AuthorizationContext context" is available,
Having a pass-up for "rfc.Errors.Add(new ExecutionError" like you can from Resolvers would be helpful.
Particularly I'm looking at wanting to use the optional pass-in "new Dictionary<String, Object>()".
When updating to the latest version of the GraphQL package (2.3.0 at the time of raising this issue), I get an exception when calling .AuthorizeWith("my-policy")
:
MissingMethodException: Method not found: 'Void GraphQL.EnumerableExtensions.Fill(System.Collections.Generic.IList`1<!!0>, !!0)'.
This seems to be because the GraphQL package removed the EnumerableExtensions.Fill
method in version 2.2.
When using AuthorizeWith on the Fields that are used for query, everything works fine but it is ignored when set on arguments that implement InputObjectGraphType. Authorization of fields in the argument is not implemented?
public class AccountInputTypeCreate : InputObjectGraphType<Account>
{
public AccountInputTypeCreate()
{
Field(x => x.Name);
Field(x => x.Value, true).AuthorizeWith(ClaimsEnum.ValueAccess.ToString());
}
}
Please help
EnumerableExtensions class in present in 2.0 but not available in 2.3.
What is alternate for => ist.Fill(policy);
public static void AuthorizeWith(this IProvideMetadata type, string policy)
{
var list = GetPolicies(type);
list.Fill(policy); // Available in 2.0 but not in 2.3
type.Metadata[PolicyKey] = list;
}
using version 3.0.39-preview-39
Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services)
{
...
services.AddGraphQL(new GraphQLOptions()
{
EnableMetrics = true,
ExposeExceptions = true,
}).AddWebSockets()
.AddUserContextBuilder(httpContext => {
return new GraphQLUserContext() { User = httpContext.User };
});
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
app.UseGraphQLWebSockets<GraphSchema>("/graphql");
app.UseGraphQL<GraphSchema>("/graphql");
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
...
}
GraphQLAutofacModule.cs
public class GraphQLAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
...
builder.RegisterType<AuthorizationEvaluator>().As<IAuthorizationEvaluator>().SingleInstance();
builder.RegisterType<AuthorizationValidationRule>().As<IValidationRule>().InstancePerLifetimeScope();
builder.Register(s =>
{
var authSettings = new AuthorizationSettings();
authSettings.AddPolicy(GraphQLAuthPolicies.Admin.ToString(), y => y.RequireClaim("role", "Admin"));
authSettings.AddPolicy(GraphQLAuthPolicies.Authenticated.ToString(), y => y.RequireClaim("id"));
return authSettings;
}).SingleInstance();
}
}
QueryType.cs
public class QueryType : ObjectGraphType
{
public QueryType(
ILifetimeScope scope
)
{
Field<ResultListType<Jurisdiction, JurisdictionType>>(
"jurisdictions",
arguments: new QueryArguments(new QueryArgument<NonNullGraphType<JurisdictionQueryInputType>> { Name = "queryObject" }),
resolve: context =>
{
return scope.Resolve<JurisdictionQueryHandler>().Handle(context.GetArgument<JurisdictionQueryRequest>("queryObject"));
}).AuthorizeWith(GraphQLAuthPolicies.Authenticated.ToString());
}
}
If I remove the AuthroizeWith call at the end of this field and create a breakpoint on the return line the UserContext looks like
If I have the AuthroizeWith() call the following error is returned
GraphQL.Validation.ValidationError: You are not authorized to run this query.↵Required claim 'id' is not present.
Not sure what I did wrong.
In the latest of graphql-dotnet UserContext
is expected to be a Dictionary<string, object>
and not an object
.
This could probably be handled by adding a simple bit:
// var userContext = context.UserContext as IProvideClaimsPrincipal;
IProvideClaimsPrinciple userContext;
if(context.UserContext is Dictionary<string, object>) {
userContext = (context.UserContext as Dictionary<string, object>)?["user"] as IProvideClaimsPrinciple;
} else {
userContext = context.UserContext as IProvideClaimsPrinciple;
}
At present the validation rule doesn’t check the returned GraphType
of the Field
that is being request, only that the current Field
itself is allowed. The returned GraphType
may have different authorization policies applied to it.
I believe the fix is to add this additional line:
CheckAuth(fieldAst, fieldDef.ResolvedType, userContext, context, operationType);
Can you tag the 2.1.29 Release like you did here:
graphql-dotnet/graphql-dotnet#1677
I checked here, but did not see it:
https://github.com/graphql-dotnet/authorization/tags
Thanks!!!
After registering the authorization classes as follows:
public static void AddGraphQLAuth(this IServiceCollection services)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.TryAddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
authSettings.AddPolicy("AdminPolicy", _ => _.RequireClaim("role", "Admin"));
return authSettings;
});
}
When a consumer tries to execute a query or a mutation that doesn't exist or to pass a parameter that doesn't exist:
mutation {
addDevelper(
id: "b07b3e4e-ea3c-42fc-9c76-40cbdec25ff5",
name:"john",
surname: "smith",
skills: [
{ Name: "C#", Score: 10 },
{ Name: "JS", Score: 7 }
])
}
The endpoint returns:
{
"errors": [
{
"message": "GraphQL.ExecutionError: Object reference not set to an instance of an object. ---> System.NullReferenceException: Object reference not set to an instance of an object.\r\n at GraphQL.Authorization.AuthorizationValidationRule.<>c__DisplayClass2_1.<Validate>b__2(ObjectField objectFieldAst)\r\n at GraphQL.Validation.EnterLeaveListener.GraphQL.Validation.INodeVisitor.Enter(INode node)\r\n at GraphQL.Validation.BasicVisitor.Visit(INode node)\r\n at
[..]
GraphQL.Validation.BasicVisitor.Visit(INode node)\r\n at GraphQL.Validation.DocumentValidator.Validate(String originalQuery, ISchema schema, Document document, IEnumerable`1 rules, Object userContext, Inputs inputs)\r\n at GraphQL.DocumentExecuter.ExecuteAsync(ExecutionOptions options)\r\n --- End of inner exception stack trace ---",
"extensions": {
"code": "NULL_REFERENCE"
}
}
]
}
This only happens if at least one parameter is an object (as the elements of the skills
array).
The expected behaviour is that the endpoint returns the same error message it would return if the authorization classes weren't registered:
{
"errors": [
{
"message": "GraphQL.Validation.ValidationError: Cannot query field \"addDevelper\" on type \"Mutation\". Did you mean \"addDeveloper\"?",
[..]
}
]
}
I don't know the library well enough to understand why context.TypeInfo.GetArgument()
is returning null
for invalid queries/mutations, but replacing line 40 of AuthorizationValidationRule.cs
:
var argumentType = context.TypeInfo.GetArgument().ResolvedType.GetNamedType() as IComplexGraphType;
with
var argumentType = context.TypeInfo.GetArgument()?.ResolvedType.GetNamedType() as IComplexGraphType;
appears to fix the problem.
If the solution is valid, I can create a PR.
I am trying to add authorization logic to an ASP.NET core application utilizing graphql-dotnet with your authorization strategy (field level, type level, etc.).
You reference some examples by pasting in a few functions (which I assume are extensions), such as UseGraphQLWithAuth and AddGraphQLWithAuth. I found in the examples where AddGraphQLWithAuth is defined in authorization/src/Harness/GraphQLAuthExtension, but could not locate UseGraphQLWithAuth. Is that something we would add or is it now baked into the graphql-dotnet/server package?
Also, I've been reading issue 502 (graphql-dotnet/graphql-dotnet#502) and wondering if this information is outdated? Is the intention that we define our own middleware code or is that logic now part of graphql-dotnet server? I ask because I've defined my own middleware that works for handling the graphql request, but (for reasons unknown) does not honor the authorization rules.
I really appreciate the graphql-dotnet stuff - I just wish I could find a a current/complete example showing this stuff in action. Any direction appreciated!
Key files below:
*** ghc.GQLAPI.csproj ***
netcoreapp2.1<PackageReference Include="graphiql" Version="1.1.0" />
<PackageReference Include="GraphQL" Version="2.3.0" />
<PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.2.0" />
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.2.0" />
<PackageReference Include="GraphQL.Authorization" Version="2.0.27"/>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.1" />
<PackageReference Include="Microsoft.AspNetCore.App"/>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.2.0" />
<!-- <PackageReference Include="GraphQL.Server.Transports.AspNetCore" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Transports.WebSockets" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Ui.GraphiQL" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Ui.Playground" Version="3.1.0" />
<PackageReference Include="GraphQL.Server.Ui.Voyager" Version="3.1.0" />
-->
*** Startup.cs ***
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using AutoMapper;
using GraphQL.Server;
using GraphQL.Http;
using GraphQL;
using GraphQL.Types;
using ghc.GQLAPI.Helpers;
using ghc.Data;
using ghc.Data.Interfaces;
using ghc.Data.Repositories;
using ghc.GQLAPI.Models;
using ghc.GQLAPI.Middleware;
using ghc.Model.Entities;
using GraphQL.Validation;
namespace ghc.GQLAPI
{
public class Startup
{
//TODO: user user secrets to avoid storing salt key in clear text
private IHostingEnvironment _env;
public IConfiguration _configuration { get; }
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
_configuration = configuration;
_env = env;
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
builder.AddEnvironmentVariables();
_configuration = builder.Build();
}
// ===DEVELOPMENT
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureDevelopmentServices(IServiceCollection services)
{
// Allow for other classes to access the configuration via dependency injection
services.AddSingleton<IConfiguration>(_configuration);
// *** Configure security/identity ****
// TODO: This is a very weak password strength policy - strengthen for prod
IdentityBuilder builder = services.AddIdentityCore<User>(opt =>
{
opt.Password.RequireDigit = false;
opt.Password.RequiredLength = 4;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireUppercase = false;
});
builder = new IdentityBuilder(builder.UserType, typeof(Role), builder.Services);
builder.AddEntityFrameworkStores<GHCOpsDbContext>(); // tell identity system to store security info in the entity framework data store
builder.AddRoleValidator<RoleValidator<Role>>();
builder.AddRoleManager<RoleManager<Role>>();
builder.AddSignInManager<SignInManager<User>>();
// Add authentication strategy
var key = Encoding.ASCII.GetBytes(_configuration.GetSection("AppSettings:TokenKey").Value);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// Allow us to reference the data seeding process in the configure method below
services.AddTransient<Seed>();
// Specify the DB Context (SqlServer)
services.AddDbContext<GHCOpsDbContext>(options =>
options.UseSqlServer(_configuration["ConnectionStrings:GHCOpsDB"],
b => b.MigrationsAssembly("ghc.Migrations.Development"))
.ConfigureWarnings( warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
);
// Add support for cross origin resource support
services.AddCors();
// Add support for mapping between domain objects and data transfer objects
services.AddAutoMapper();
// BEGIN: GRAPHQL $$$$$$$$$$$$$$$$$$$
// Extension method to allow for GraphQL Authorization logic
services.AddGraphQLAuth(options =>
{
options.AddPolicy("AdminPolicy", p => p.RequireClaim("role", "Administrator"));
});
// Add GraphQL services and configure options
services.AddGraphQL(options =>
{
options.EnableMetrics = true;
options.ExposeExceptions = _env.IsDevelopment();
})
.AddWebSockets() // Add required services for web socket support
.AddDataLoader() // Add required services for DataLoader support
.AddUserContextBuilder(httpContext => new GraphQLUserContext { User = httpContext.User });
services.AddSingleton<IDocumentWriter, DocumentWriter>();
services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
services.AddSingleton<GHCQuery>();
services.AddSingleton<GHCMutation>();
services.AddScoped<IValuesRepository, ValuesRepository>();
services.AddScoped<IUsersRepository, UsersRepository>();
services.AddScoped<IClientsRepository, ClientsRepository>();
services.AddScoped<ICaregiversRepository, CaregiversRepository>();
services.AddScoped<IKeywordsRepository, KeywordsRepository>();
services.AddScoped<INotesRepository, NotesRepository>();
services.AddScoped<IPhotosRepository, PhotosRepository>();
services.AddSingleton<ghc.GQLAPI.Models.ValueType>(); // had to fully qualify since this conflicts with existing asp.net framework type
services.AddSingleton<ClientType>();
services.AddSingleton<UserType>();
services.AddSingleton<CreateUserInputType>();
services.AddSingleton<UpdateUserInputType>();
services.AddSingleton<NoteType>();
services.AddSingleton<PhotoType>();
services.AddSingleton<CaregiverType>();
services.AddSingleton<RecruitingStatusType>();
services.AddSingleton<ServiceStatusType>();
// Do note move these two lines above the graphql services
var sp = services.BuildServiceProvider();
services.AddSingleton<ISchema>(new GHCSchema(new FuncDependencyResolver(type => sp.GetService(type))));
// END: GRAPHQL $$$$$$$$$$$$$$$$$$$$$
// Add Support for Model View Controller Framework
services.AddMvc(options =>
{
var mustBeAuthenticatedPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(mustBeAuthenticatedPolicy));
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(opt => opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, Seed seeder)
{
Console.WriteLine("=========================================");
Console.WriteLine("Environment: " + (env.EnvironmentName).ToUpper());
Console.WriteLine("=========================================");
// Ensures migrations have been run or creates the DB from scratch if not found
UpdateDatabase(app);
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else {
// Establish a global exception handler in case we are not in Development mode
app.UseExceptionHandler(builder => {
builder.Run(async context => {
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null) {
context.Response.AddApplicationError(error.Error.Message);
await context.Response.WriteAsync(error.Error.Message);
}
});
});
}
// Cross Origin Resource - Order matters - this must come before UseMvc()
// TODO: Tighten up security here
app.UseCors(x => x.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials());
app.UseWebSockets(); // this is required for websockets support at the ASP.NET CORE level
app.UseGraphQL<ISchema>("/graphqlmw");
app.UseGraphiQl("/graphiql"); // must come before app.UseMvc()!!
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
private static void UpdateDatabase(IApplicationBuilder app)
{
using (var serviceScope = app.ApplicationServices
.GetRequiredService<IServiceScopeFactory>()
.CreateScope())
{
using (var context = serviceScope.ServiceProvider.GetService<GHCOpsDbContext>())
{
context.Database.Migrate();
}
}
}
}
}
*** GraphQLAuthExtensions ***
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using GraphQL.Authorization;
using GraphQL.Validation;
using Microsoft.AspNetCore.Builder;
using System.Threading.Tasks;
using ghc.GQLAPI.Middleware;
namespace ghc.GQLAPI.Helpers
{
public static class GraphQLAuthExtensions
{
public static void AddGraphQLAuth(this IServiceCollection services, Action<AuthorizationSettings> configure)
{
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.TryAddSingleton<IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<IValidationRule, AuthorizationValidationRule>();
services.TryAddSingleton(s =>
{
var authSettings = new AuthorizationSettings();
configure(authSettings);
return authSettings;
});
}
}
}
*** ValueType.cs with Authorization specs for a simple type***
using ghc.Model.Entities;
using GraphQL.Authorization;
using GraphQL.Types;
namespace ghc.GQLAPI.Models
{
public class ValueType : ObjectGraphType<Value>
{
public ValueType()
{
// this.AuthorizeWith("AdminPolicy"); // this can be used to protect the entire type
Name = "Value";
Field(x => x.Id).Description("The Value Id");
Field(x => x.Created).Description("Date & Time item was created");
Field<StringGraphType>("Name", "This is the name").AuthorizeWith("AdminPolicy");
}
}
}
*** GraphQLMiddleware.cs - currently not using since I could not get the code to honor authorization rules ***
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using GraphQL.Types;
using Microsoft.AspNetCore.Authorization;
using Newtonsoft.Json.Linq;
using ghc.GQLAPI.Helpers;
using System.Net;
namespace ghc.GQLAPI.Middleware
{
// DDD: This middleware is redundant with the graphqlcontroller, not sure if I should use this code or the controller.
public class GraphQLMiddleware
{
private readonly RequestDelegate _next;
private readonly GraphQLSettings _settings;
private readonly IDocumentExecuter _executor;
private readonly IDocumentWriter _writer;
public GraphQLMiddleware(RequestDelegate next, IDocumentWriter writer, IDocumentExecuter executor, GraphQLSettings settings = null)
{
System.Diagnostics.Debugger.Break();
Console.WriteLine("===================================== INITIALIZING GQL middleware =====================================");
_next = next;
_writer = writer;
_executor = executor;
_settings = settings;
}
public async Task InvokeAsync(HttpContext httpContext, ISchema schema)
{
Console.WriteLine("======== Calling GQL middleware");
if (!IsGraphQLRequest(httpContext))
{
await _next(httpContext);
return;
}
await ExecuteAsync(httpContext, schema);
}
private async Task ExecuteAsync(HttpContext httpContext, ISchema schema)
{
var request = Deserialize<GraphQLRequest>(httpContext.Request.Body);
if (request.Query == null) { throw new ArgumentNullException(nameof(request.Query)); }
var response = await _executor.ExecuteAsync(options =>
{
options.Schema = schema;
options.Query = request.Query;
options.OperationName = request.OperationName;
options.Inputs = request.Variables.ToInputs();
options.UserContext = _settings.BuildUserContext?.Invoke(httpContext);
}).ConfigureAwait(false);
if (response.Errors?.Count > 0)
{
var errors = WriteErrors(response);
ExecutionResult executionResult = new ExecutionResult { Errors = errors };
await WriteResponseAsync(httpContext, executionResult, (int)HttpStatusCode.BadRequest);
}
else
{
await WriteResponseAsync(httpContext, response, (int)HttpStatusCode.OK);
}
}
private bool IsGraphQLRequest(HttpContext context)
{
return context.Request.Path.StartsWithSegments(_settings.Path) && string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase);
}
private async Task WriteResponseAsync(HttpContext httpContext, ExecutionResult result, int httpStatusCode)
{
var json = new DocumentWriter(indent: true).Write(result);
httpContext.Response.StatusCode = httpStatusCode;
httpContext.Response.ContentType = _settings.ResponseContentType;
await httpContext.Response.WriteAsync(json);
}
private ExecutionErrors WriteErrors(ExecutionResult result)
{
var errors = new ExecutionErrors();
foreach (var error in result.Errors)
{
var ex = new ExecutionError(error.Message);
if (error.InnerException != null)
{
ex = new ExecutionError(error.Message, error.InnerException);
}
errors.Add(ex);
}
return errors;
}
public static T Deserialize<T>(Stream s)
{
using (var reader = new StreamReader(s))
using (var jsonReader = new JsonTextReader(reader))
{
var ser = new JsonSerializer();
return ser.Deserialize<T>(jsonReader);
}
}
private async Task WriteResponseAsync(HttpContext context, ExecutionResult result)
{
var json = _writer.Write(result);
context.Response.ContentType = "application/json";
context.Response.StatusCode = result.Errors?.Any() == true ? (int)HttpStatusCode.BadRequest : (int)HttpStatusCode.OK;
await context.Response.WriteAsync(json);
}
}
public class GraphQLRequest
{
public string Query { get; set; }
public string OperationName { get; set; }
public JObject Variables { get; set; }
}
}
Using the GraphQLAuthorize attribute, can I apply multiple policies at once ...
like ["Admin", "Teacher"] .
Then any user that has any of those claims get authorized.
Is it possible to white list one or more endpoints?
for example: I would like to require authentication for all mutation and queries except for Login.
Currently we authorize each mutation individually - we fear forgetting one will ship and endpoint with open access. The query mutation we AuthorizeWith at the root level therefore it catches all endpoints.
If this is not possible with the current implementation, I am happy to create a PR but how would this work? Instead of AuthorizeWith on an endpoint have DisableAuthorization() ? This could add metadata that is inspected during evaluation to bypass root level policies. What are the drawbacks to this approach?
Update
Took the initial approach of removing operation level validation and the fields now inherit parent policies if they are not flagged a public.
First of all thank you so much for your great work. It's really fun using GraphQL!
Is there on option to authorize an object depending on a property value after getting the object from a databasebase?
In my special case I need to do authorization depending on a resource ownership. The user is allowed to query for objects of a specific type, but only if he is the owner of the resource.
I already played with the leave action of the INodeVisitor "Validate" method, but the AuthorizationContext in IAuthorizationRequirement.Authorize does not provide any Information about the resource.
Am I missing something here?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.