Get ClaimsPrincipal from bearer access token in custom AuthorizeAttribute - asp.net-web-api

I can get bearer access token from embedded authorization server.
The problem is when I request resource from WebAPI, I could not get back the ClaimsPrincipal based on the access token sent along.
Below is the Http post: (1)
GET /api/v1/courses/categories HTTP/1.1
Host: localhost:8080
Authorization: Bearer QoHxP-MbGna2ekhZl-zV9C_3gW2LPBbe-i7DQDp1DFF0BGurjAsqx-rShhmTZlnvScnAdZHNnR1PLcVVuz8B3VwvsYwKKvp-tYw3sohf4B2m5IH97FeCRbA9_nv4DflZtUSSQnrdlKNHHQGgl8VZ5Pcdc09V5hAv5f3sGd-1qsIsKuON81mhlC2PKWNA6a0NTid_LyvzEY1zGQtrgx8ue2NFMNztXBYuy1jXgv6SPAdoOLU3mH38wXg4l7UmVJ10MAbcljNczDSgY9QOC439BmpscG_kIYONiqQaMGMQhmFqnaKIkCMy9PgQ9WvQaLpz
Cache-Control: no-cache
Postman-Token: bd0096e6-3471-bb49-460d-f198f7f516e4
Below is the Owin Startup: (2)
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseWebApi(config);
app.UseCors(CorsOptions.AllowAll);
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new AuthorizationServerProvider(),
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
Below is the WebAPI configuration: (3)
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Filters.Add(new CustomAuthorizeAttribute());
config.SuppressDefaultHostAuthentication();
config.SuppressHostPrincipal();
config.Formatters.OfType<JsonMediaTypeFormatter>().First().SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
}
Grant access token: (4)
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
await Task.FromResult(context.Validated());
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType); // using Bearer
identity.AddClaim(new Claim("AccountId", Guid.NewGuid()));
context.Validated(identity);
}
}
Lastly here is the problem part: (5)
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
{
// I tried to access the - ((ClaimsPrincipal)actionContext.RequestContext.Principal).Claims
}
}
I tried to access the ClaimsPrincipal, but was empty, I need the "AccountId" which was set during GrantResourceOwnerCredentials() - look code at (4).

Related

Authenticate Keycloak JWT token outside Spring Boot filter chain

I have a web application (Spring Boot + Angular). The backend authentication is implemented using Keycloak (JWT).
I need to create a GraphQL subscription using Apollo and the subscription should be authenticated/authorized. The subscription is using websocket and regrettably it is impossible to add authorization headers to the initial HTTP request. There are two ways in which the websocket can be authenticated: 1) insert the JWT token into the request URI, and 2) add token to the init message.I would prefer to use the second way as I do not want the token to be stored in any logs.
The request is being created with this code:
const auth = this.injector.get(AuthenticationService);
connections
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:9090/web-service/graphql_ws',
connectionParams: {
Authorization: 'Bearer ' + auth.getLoginDataFromStorage().access_token
}
}))
const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([
middleware,
errorLink,
split(
// split based on operation type
({ query }) => {
const def = getMainDefinition(query)
return def.kind === 'OperationDefinition' && def.operation === 'subscription'
},
wsLink,
httpLink
)
])
,
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache'
},
query: {
fetchPolicy: 'no-cache'
}
},
});
I have created a WebSocketGraphQlInterceptor in order to retrieve the token during the connection initialisation, however I struggle with authenticating the session using this token.
Interceptor:
#Configuration
public class SubscriptionInterceptor implements WebSocketGraphQlInterceptor
{
#Override
public Mono<Object> handleConnectionInitialization(
WebSocketSessionInfo sessionInfo, Map<String, Object> connectionInitPayload)
{
var authToken = connectionInitPayload.get("Authorization").toString();
return Mono.just(connectionInitPayload);
}
#Override
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain)
{
List<String> token = request.getHeaders().getOrEmpty("Authorization");
return chain.next(request)
.contextWrite(context -> context.put("Authorization", token.isEmpty() ? "" : token.get(0)));
}
}
Security configuration:
#Configuration
#EnableWebSecurity
#ConditionalOnProperty(value = "keycloak.enabled", matchIfMissing = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.anyRequest().permitAll();
}
}
How would I be able to authenticate the user using the JWT token in the interceptor?

How to configure bearer-only = true when Using spring-boot-starter-oauth2-client

I am trying to setup o auth2 authentication in spring cloud gateway for my rest apis using keycloak. keyclcoak redirects my request to login page while passing access token as bearer token. In many places I found solution for this is to set bearer-only = true in keycloak adapter. where to set this while using spring-boot-starter-oauth2-client. I cannot use keycloak-spring-boot-starter to set this in application.yml
Thanks
I had the same question as you. Not having found an answer, I developed a filter with a light keycloak Client that calls the Endpoint instrospect of keycloak to validate the token
the client:
public class KeycloakClient {
private final KeycloakConfigurationProperties kcProperties;
private final WebClient client;
public KeycloakClient(final KeycloakConfigurationProperties keycloakConfigurationProperties) {
this.kcProperties = keycloakConfigurationProperties;
this.client = WebClient.builder()
.baseUrl(keycloakConfigurationProperties.getAuth_server_url())
.filter(logRequest())
.build();
}
public Mono<Boolean> validateBearerToken(String bearerToken) {
//todo: improve error management
return prepareAndRunRequest(bearerToken).retrieve()
.bodyToMono(KeycloakValidationResponse.class)
.flatMap(response -> Mono.just(response.getActive()))
.onErrorResume(WebClientResponseException.class,
ex -> Mono.just(false));
}
private WebClient.RequestHeadersSpec<?> prepareAndRunRequest(String bearerToken) {
return client.post()
.uri(uriBuilder -> uriBuilder.path("/auth/realms/")
.path(kcProperties.getRealm())
.path("/protocol/openid-connect/token/introspect")
.build())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromFormData("client_id", kcProperties.getResource())
.with("client_secret", kcProperties.getCredentials_secret())
the filter:
public class ValidationTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<ValidationTokenGatewayFilterFactory.Config> {
private final KeycloakClient client;
public ValidationTokenGatewayFilterFactory(KeycloakClient client) {
super(Config.class);
this.client = client;
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String token = exchange.getRequest()
.getHeaders()
.get(AUTHORIZATION)
.get(0);
token = token.substring(7);
log.trace("-- ValidationToken(): token={}", token);
return client.validateBearerToken(token)
.flatMap(validated -> {
if (validated) {
log.debug("-- ValidationToken(): Token valid");
return chain.filter(exchange);
} else {
log.debug("-- ValidationToken(): Token invalid");
exchange.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse()
.setComplete();
}
});
};
}
public static class Config {
}
}
You can find a full sample here: https://gitlab.com/-/snippets/2105967

Why does Spring Security + Angular login have different sessions on AuthenticationSuccessHandler and RestController?

I have a Spring Security configuration and a login page in Angular. After the successfull login, my SimpleAuthenticationSuccessHandler redirects me to a controller that gets the user from session and returns it. When I call the login from Postman, everything goes as expected, but when I call it from Chrome it doesn't work, because the session on SimpleAuthenticationSuccessHandler is different than the session received on controller.
This is the configuration class for the Spring Security:
#Configuration
#EnableWebSecurity
#ComponentScan("backend.configuration")
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableMongoRepositories(basePackages = "backend.repositories")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/user/").authenticated()
.and()
.formLogin()
.usernameParameter("email")
.loginProcessingUrl("/login").
successHandler(authenticationSuccessHandler())
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.and()
.logout();
}
#Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new SimpleOnSuccessAuthenticationHandler();
}
#Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
UrlBasedCorsConfigurationSource source = new
UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
This is the custom authentication success handler:
public class SimpleOnSuccessAuthenticationHandler
implements AuthenticationSuccessHandler {
protected Log logger = LogFactory.getLog(this.getClass());
#Autowired
UserRepository userRepository;
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException {
handle(request, response, authentication);
clearAuthenticationAttributes(request);
}
protected void handle(HttpServletRequest request,
HttpServletResponse response, Authentication
authentication)
throws IOException {
HttpSession session = request.getSession();
ObjectId objectId = ((MongoUserDetails)
authentication.getPrincipal()).getId();
User loggedUser = userRepository.findById(objectId).orElse(null);
UserDto loggedUserDto = UserConverter.convertUserToDto(loggedUser);
session.setAttribute("loggedUser", loggedUserDto);
if (response.isCommitted()) {
logger.debug(
"Response has already been committed. Unable to redirect to "
+ "/loginSuccess");
return;
}
redirectStrategy.sendRedirect(request, response, "/loginSuccess");
}
protected void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
This is the controller that returns the user:
#CrossOrigin
#RestController
public class LoginController {
#Autowired
UserService userService;
#RequestMapping(value = "/loginSuccess", method = RequestMethod.GET,
produces = "application/json")
#ResponseBody
public ResponseEntity<UserDto> login(HttpServletRequest request) {
UserDto loggedUser= (UserDto)
request.getSession().getAttribute("loggedUser");
System.out.println(request.getSession().getId());
System.out.println(request.getSession().getCreationTime());
return new ResponseEntity<>((UserDto)
request.getSession().getAttribute("loggedUser"), HttpStatus.OK);
}
}
The angular auth.service.ts:
#Injectable({providedIn: 'root'})
export class AuthService {
apiURL = environment.apiUrl;
constructor(private http: HttpClient) {}
login(username: string, password: string) {
let body = new URLSearchParams();
body.set('email', username);
body.set('password', password);
let options = {headers: new HttpHeaders().set('Content-Type',
'application/x-www-form-urlencoded')
};
return this.http.post(this.apiURL + 'login', body.toString(), options);
}
logout() {localStorage.removeItem('currentUser');}
}
And the login.component.ts is :
#Component({selector: 'app-login',templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
user = {} as any;
returnUrl: string;
form: FormGroup;
formSubmitAttempt: boolean;
errorMessage: string = '';
welcomeMessage: string = 'Welcome to CS_DemandResponse Application';
url = '/add_user';
token: string;
constructor(
private fb: FormBuilder,
private authService: AuthService,
private route: ActivatedRoute,
private router: Router
) {
}
ngOnInit() {
this.authService.logout();
this.returnUrl = this.route.snapshot.queryParams.returnUrl || '/';
this.form = this.fb.group({
email: [AppConstants.EMPTY_STRING, Validators.email],
password: [AppConstants.EMPTY_STRING, Validators.required]
});
}
isFieldInvalid(field: string) {
return (
(!this.form.get(field).valid && this.form.get(field).touched) ||
(this.form.get(field).untouched && this.formSubmitAttempt)
);
}
login() {
if (this.form.valid) {
this.authService.login(this.user.email, this.user.password)
.subscribe((currentUser) => {
this.user=currentUser;
if (this.user != null) {
localStorage.setItem('userId', (<User>this.user).id.toString());
if (this.user.autorities.get(0) === 'ROLE_ADMIN' ) {
this.router.navigate(['/admin']);
}
if (this.user.autorities.get(0) === 'ROLE_USER') {
// this.route.params.subscribe((params) => {
// localStorage.setItem('userId', params.id);
// });
this.router.navigate(['/today']);
}
} else {
this.errorMessage = ('Invalid email or password');
this.welcomeMessage = '';
}
});
this.formSubmitAttempt = true;
}
}
}
The /loginSuccess controller returns null so the login.component.ts receives a null on the subscribe.
I assume this is because Spring "exchanges" your session on successfull authentication if you had one, to prevent certain attacks.
Someone could "steal" your session-cookie while unauthenticated and then use it -when you logged in - to also access protected resources, using your now authenticated session.
If you never had a session - eg. when executing the login-request via Postman - there never was a point in the session where you where "unsafe" - so Spring does not have to do this.
You can verify this by requesting your login page in postman, copying the sessionId you get and setting it as session-cookie in your login request. If i am correct you will then be assigned a new session.

Access-Control-Allow-Origin error with DELETE, while working fine with GET / POST

BackEnd is Spring, I'v configured CORS like this
#SpringBootApplication
public class App {
public static void main(String args[]){
SpringApplication.run(App.class, args);
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
};
}
}
Now I got following code in Controller
#PostMapping("/add")
public ProductDto addProduct(#Valid #RequestBody ProductDto productDto){
return productService.addProduct(productDto);
}
#RequestMapping(path="/remove/{id}", method=RequestMethod.DELETE)
#ResponseBody
public String removeProduct(#PathVariable Long id) {
return productService.removeProduct(id);
}
And from Angular 6 FrontEnd I'm calling those 2 endpoints
let httpHeaders = new HttpHeaders({
'Content-Type' : 'application/json',
});
let options = {
headers: httpHeaders
};
addProduct() {
const product = new Product();
product.name = this.productNameValue;
product.categoryName = this.categoryValue;
product.kcal = this.caloriesValue;
product.protein = this.proteinValue;
product.fat = this.fatValue;
product.carb = this.carbsValue;
this.http.post('http://localhost:8080/product/add', JSON.stringify(product), options).subscribe(data => this.populateProductTable());
}
removeProduct(x: any) {
const url = 'http://localhost:8080/product/remove/' + x.id;
this.http.delete(url, options).subscribe(data => console.log(data));
}
First one (and similar GET method) works fine, when I try to use DELETE, I got
Failed to load http://localhost:8080/product/remove/2: Response to
preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://localhost:4200' is therefore not allowed
access.
You need to add DELETE http-verb
For Spring Web MVC
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
}
}
For Spring Boot:
#Configuration
public class MyConfiguration {
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH");
}
};
}
}
To know how CORS works with spring, refer:
https://spring.io/blog/2015/06/08/cors-support-in-spring-framework#javaconfig
Spring security CORS Filter

Owin middleware could not registered with Autofac

In my project, I'm using the web api 2, autofac, in the controller I inject to it, everything works fine until I installed The Owin to project, when I run project and navigate to the controller that threw me the error:
"An error occurred when trying to create a controller of type 'AccountController'. Make sure that the controller has a parameterless public constructor."
Here is my code:
The AccountController.cs:
[RoutePrefix("api/Account")]
public class AccountController : ApiControllerBase
{
private readonly IAccountService _accountService;
public AccountController(IAccountService accountService,
IEntityBaseRepository<Error> errorsRepository)
: base(errorsRepository)
{
_accountService = accountService;
}
// POST api/Account/Register
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(UserModel userModel)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
IdentityResult result = await _repo.RegisterUser(userModel);
IHttpActionResult errorResult = GetErrorResult(result);
if (errorResult != null)
{
return errorResult;
}
return Ok();
}
}
Startup.cs:
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
HttpConfiguration config = new HttpConfiguration();
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider()
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
AutofacWebApiConfig.cs:
public class AutofacWebapiConfig
{
public static IContainer Container;
public static void Initialize(HttpConfiguration config)
{
Initialize(config, RegisterServices(new ContainerBuilder()));
}
public static void Initialize(HttpConfiguration config, IContainer container)
{
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
private static IContainer RegisterServices(ContainerBuilder builder)
{
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
// EF BasicCommerceContext
builder.RegisterType<BasicCommerceContext>()
.As<DbContext>()
.InstancePerRequest();
builder.RegisterType<DbFactory>()
.As<IDbFactory>()
.InstancePerRequest();
builder.RegisterType<UnitOfWork>()
.As<IUnitOfWork>()
.InstancePerRequest();
builder.RegisterGeneric(typeof(EntityBaseRepository<>))
.As(typeof(IEntityBaseRepository<>))
.InstancePerRequest();
// Services
builder.RegisterType<EncryptionService>()
.As<IEncryptionService>()
.InstancePerRequest();
builder.RegisterType<MembershipService>()
.As<IMembershipService>()
.InstancePerRequest();
builder.RegisterType<ProductCategoryService>()
.As<IProductCategoryService>()
.InstancePerRequest();
builder.RegisterType<ProductService>()
.As<IProductService>()
.InstancePerRequest();
builder.RegisterType<NewsCategoryService>()
.As<INewsCategoryService>()
.InstancePerRequest();
builder.RegisterType<NewsService>()
.As<INewsService>()
.InstancePerRequest();
builder.RegisterType<CustomerService>()
.As<ICustomerService>()
.InstancePerRequest();
builder.RegisterType<StaticService>()
.As<IStaticService>()
.InstancePerRequest();
// Generic Data Repository Factory
builder.RegisterType<DataRepositoryFactory>()
.As<IDataRepositoryFactory>().InstancePerRequest();
Container = builder.Build();
return Container;
}
}
Please support me to register or config OWIN with Autofac.
Thank you very much.
Please follow the below steps
Reference the Autofac.Owin package from NuGet.
Build your Autofac container.
Register the Autofac middleware with OWIN and pass it the container.
Documentation is available here

Resources