Hi standing up a small FHIR v4 server with HAPI. I'm not great with Spring/Java having a hard time figuring out how to configure openapi with HAPI FHIR. Specifically I want to configure openapi to provide a button to authenticate users (with an implicit flow against my IDP) before allowing them to hit my FHIR endpoints. Does the built in HAPI OpenApiInterceptor have a way to provide an authentication mechinism?
https://hapifhir.io/hapi-fhir/docs/server_plain/openapi.html
To clarify I can add the interceptor and get the swagger page to get served but I cannot figure out how to configure the interceptor such that I can provide a mechinism to authenticate users. This code block works but doesn't appear to provide me a way to configure swagger auth.
#Override
protected void initialize() throws ServletException {
// ... define your resource providers here ...
// Now register the interceptor
OpenApiInterceptor openApiInterceptor = new OpenApiInterceptor();
registerInterceptor(openApiInterceptor);
}
OpenApiInterceptor does not performs authorization of users.
Hapi has provided us various interceptors to perform some tasks
in your case you should refer this, it will give you some idea.
Hapi fhir authorization interceptor
You can extend the OpenApiInterceptor class and #Override the "generateOpenApi" method to add your extra needed OpenAPI configuration after calling the parent method.
public class CustomOpenApiInterceptor extends OpenApiInterceptor {
...
#Override
protected OpenAPI generateOpenApi(ServletRequestDetails theRequestDetails) {
OpenAPI openApi = super.generateOpenApi(theRequestDetails);
// Add Authentication to OAS spec.
openApi.getComponents().addSecuritySchemes("oauth2schema", oauth2ImplicitSecurityScheme());
SecurityRequirement securityRequirement = new SecurityRequirement();
securityRequirement.addList("oauth2schema");
openApi.security(Collections.singletonList(securityRequirement));
return openApi;
}
...
}
Related
I have a Micronaut microservice that handles authentication via JsonWebTokens (JWT) from this guide.
Now I'd like to extend this code. The users in my app have some extra attributes such as email, adress, teamId etc. I have all users in the database.
How do I know in the backend controller method which user corresponds to the JWT that is sent by the client?
The guide contains this example code for the Micronaut REST controller:
#Secured(SecurityRule.IS_AUTHENTICATED)
#Controller
public class HomeController {
#Produces(MediaType.TEXT_PLAIN)
#Get
public String index(Principal principal) {
return principal.getName();
}
}
I know that I can get the name of the principal, ie. the username from the HttpRequest. But how do I get my additional attributes?
(Maybe I misunderstand JWT a bit???)
Are these JWT "claims" ?
Do I need to load the corresponding user by username from my DB table?
How can I verify that the sent username is actually valid?
edit Describing my usecase in more detail:
Security requirements of my use case
Do not expose valid information to the client
Validate everything the client (a mobile app) sends via REST
Authentication Flow
default oauth2 flow with JWTs:
Precondition: User is already registerd. Username, hash(password) and furhter attributes (email, adress, teamId, ..) are known on the backend.
Client POSTs username and password to /login endpoint
Client receives JWT in return, signed with server secret
On every future request the client sends this JWT as bearer in the Http header.
Backend validates JWT <==== this is what I want to know how to do this in Micronaut.
Questions
How to validate that the JWT is valid?
How to and where in which Java class should I fetch additional information for that user (the additional attributes). What ID should I use to fetch this information. The "sub" or "name" from the decoded JWT?
How do I load a “user” in a micronaut backend when JWT is provided?
I am reading this as you plan to load some kind of User object your database and access it in the controller.
If this is the case you need to hook into the place where Authentication instance is created to read the "sub" (username) of the token and then load it from the database.
How to extend authentication attributes with more details ?
By default for JWT authentication is created using JwtAuthenticationFactory and going more concrete default implementation is DefaultJwtAuthenticationFactory. If you plan to load more claims this could be done by replacing it and creating extended JWTClaimsSet or your own implementation of Authentication interface.
How do I access jwt claims ?
You need to check SecurityService -> getAuthentication() ->getAttributes(), it returns a map of security attributes which represent your token serialised as a map.
How to validate that the JWT is valid?
There is a basic validation rules checking the token is not expired and properly signed, all the rest validations especially for custom claims and validating agains a third parties sources have to be done on your own.
If you plan to validate your custom claims, I have already open source a project in this scope, please have a look.
https://github.com/traycho/micronaut-security-attributes
How to extend existing token with extra claims during its issuing ?
It is required to create your own claims generator extending JWTClaimsSetGenerator
#Singleton
#Replaces(JWTClaimsSetGenerator)
class CustomJWTClaimsSetGenerator extends JWTClaimsSetGenerator {
CustomJWTClaimsSetGenerator(TokenConfiguration tokenConfiguration, #Nullable JwtIdGenerator jwtIdGenerator, #Nullable ClaimsAudienceProvider claimsAudienceProvider) {
super(tokenConfiguration, jwtIdGenerator, claimsAudienceProvider)
}
protected void populateWithUserDetails(JWTClaimsSet.Builder builder, UserDetails userDetails) {
super.populateWithUserDetails(builder, userDetails)
// You your custom claims here
builder.claim('email', userDetails.getAttributes().get("email"));
}
}
How do I access jwt claims ?
If you want to access them from the rest handler just add io.micronaut.security.authentication.Authentication as an additional parameter in the handling method. Example
#Get("/{fooId}")
#Secured(SecurityRule.IS_AUTHENTICATED)
public HttpResponse<Foo> getFoo(long fooId, Authentication authentication) {
...
}
I found a solution. The UserDetails.attributes are serialized into the JWT. And they can easily be set in my CustomAuthenticationProviderclass:
#Singleton
#Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
#Override
public Publisher<AuthenticationResponse> authenticate(
#Nullable HttpRequest<?> httpRequest,
AuthenticationRequest<?, ?> authenticationRequest)
{
// ... autenticate the request here ...
// eg. via BasicAuth or Oauth 2.0 OneTimeToken
// then if valid:
return Flowable.create(emitter -> {
UserDetails userDetails = new UserDetails("sherlock", Collections.emptyList(), "sherlock#micronaut.example");
// These attributes will be serialized as custom claims in the JWT
Map attrs = CollectionUtils.mapOf("email", email, "teamId", teamId)
userDetails.setAttributes(attrs);
emitter.onNext(userDetails);
emitter.onComplete();
}, BackpressureStrategy.ERROR);
}
}
And some more pitfalls when validating the JWT in the backend
A JWT in Micronaut MUST contain a "sub" claim. The JWT spec does not require this, but Micronaut does. The value of the "sub" claim will become the username of the created UserDetails object.
If you want to load addition attributes into these UserDetails when the JWT is validated in the backend, then you can do this by implementing a TokenValidator. But (another pitfal) then you must set its ORDER to a value larger than micronaut's JwtTokenValidator. Your order must be > 0 otherwise your TokenValidator will not be called at all.
By default, Spring Data REST will expose search resources to urls under {resource_name}/search. For example, findBySubject_Id in the following code will be exposed to {baseUrl}/questions/search/findBySubject_Id?subjectId={subjectId}.
public interface QuestionRepository extends PagingAndSortingRepository<Question, String> {
Page<Question> findBySubject_Id(#Param("subjectId")String subjectId, Pageable pageable);
}
For compatiblility reasons, I need the exposing link to be {baseUrl}/questions?subjectId={subjectId}. Is there any way to do it?
You can not override /search url. If you really need a link without search, you should override response handler for the entity via writing your own controller with annotation #RepositoryRestController
I am developing a REST API with Fantom and afBedSheet. I need to allow cross-origin resource sharing so that I can call my RESTful services via AJAX from the UI which runs on a different web container on a different port.
I am currently doing this in request handler methods:
res.headers["Access-Control-Allow-Origin"] = "http://localhost:8080"
But as the API grows and the number of request handlers grow, it is no longer practical. I'm wondering how can I inject that header in every response. I have Googled the question but only found a reference to a document from a very old version of afBedSheet which doesn't seem relevant anymore. Can anyone provide an example, please?
CORS has to be set up manually but as mentioned, it's not that difficult. Anything that becomes repetitive in request handler methods can usually be squirrelled away somewhere, and setting HTTP response headers is no different. These can be set via BedSheet Middleware:
using afIoc
using afBedSheet
const class CorsMiddleware : Middleware {
#Inject private const HttpRequest req
#Inject private const HttpResponse res
#Inject private const ResponseProcessors processors
new make(|This|in) { in(this) }
override Void service(MiddlewarePipeline pipeline) {
// echo back in the response, whatever was sent in the request
res.headers["Access-Control-Allow-Origin"] = req.headers["Origin"]
res.headers["Access-Control-Allow-Methods"] = req.headers["Access-Control-Request-Method"]
res.headers["Access-Control-Allow-Headers"] = req.headers["Access-Control-Request-Headers"]
// deal with any pre-flight requests
if (req.httpMethod == "OPTIONS")
processors.processResponse(Text.fromPlain("OK"))
else
pipeline.service
}
}
Note that the above will enable CORS on all requests - handy for dev, but for live code you should be more choosy and validate any given Origins, Methods, and Headers.
BedSheet Middleware should be contributed to the MiddlewarePipeline service:
#Contribute { serviceType=MiddlewarePipeline# }
static Void contributeMiddleware(Configuration config) {
config.set("myApp.cors", config.autobuild(CorsMiddleware#)).before("afBedSheet.routes")
}
Note that CorsMiddleware is inserted into the pipeline before BedSheet routes to ensure it gets executed.
I'm using jboss keycloak 1.5 final version.
I developed my custom user federation provider interfacing with keycloak properties and my user enterprise database.
My need is to send up to user the login interface custom error messages based on particular specific error related to my legacy user db.
I saw keycloak themes have a resources folder by which i can localize and add new messages. Then i can reference them by angular js using
$myMessage
notation. The problem is i want to rise up a message from keycloak server. My user federation provider implements UserFederationProvider interface. So i should have to override:
#Override
public CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel credential) {
LOGGER.info("validCredentials(realm, credential)");
return CredentialValidationOutput.failed();
}
which seems to be the method i was looking for just because CredentialValidationOutput contains custom messages to be sent as validation output. The problem is this method is never called.
Why?
I'll post the answer found on my own.
It's necessary to develop your own Authenticator. For example refer to Keycloak UsernameAndForm and UsernameAndFormFactory implementation.
You can find them on Keycloak github source code:
https://github.com/keycloak/keycloak/tree/master/services/src/main/java/org/keycloak/authentication/authenticators/browser
The main validation method are:
public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
...
}
public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData) {
...
}
From your custom user federation provider you can throw your custom exception and catch them in the two methods above adding:
catch (YourCustomException ex){
...
Response challengeResponse = context.form()
.setError("YOUR ERROR MESSAGE", me.getMandator()).createLogin();
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
return false;
}
Of course in your project you have to add
META-INF/service/org.keycloak.authentication.AuthenticatorFactory
In which you specify the full qualified name of your AuthenticatorFactory.
For a valid guide make reference to Keycloak User Guide 1.6.1 Final. Chapter 33.3
I use Apache Camel like a smart HTTP proxy, in front of REST APIs. I have a configuration file with routes to configure and it works great.
To avoid complexity, I will summerize the code by :
camelContext.addRoutes(new RouteBuilder(){
#Override
public void configure() throws Exception {
from("servlet:///v1.3?matchOnUriPrefix=true")
.to("http4://localhost:8080/my-rest-api-v1.3.0?bridgeEndpoint=true&throwExceptionOnFailure=false");
from("servlet:///v1.2?matchOnUriPrefix=true")
.to("http4://localhost:8080/my-rest-api-v1.2.1?bridgeEndpoint=true&throwExceptionOnFailure=false");
}
});
My problem is on the endpoint server. When I retrieve the Request URL from my HttpServletRequest, it gives me a "http://localhost:8080/my-rest-api-v1.3.0/resources/companies/" instead of "http://my.site.com/my-rest-api" (which is the URL of my proxy).
How can I transfer requested host name to my endpoint?
I don't find how to do it with Apache Camel.
HTTP request has properies (like 'host') in its Header and to use this property in camel you need just replace localhost:8080 with ${header.host} and use recipientList EIP (so you can use Simple language to create an URI):
camelContext.addRoutes(new RouteBuilder(){
#Override
public void configure() throws Exception {
from("servlet:///v1.3?matchOnUriPrefix=true")
.recipientList(simple("http4://${header.host}/my-rest-api-v1.3.0?bridgeEndpoint=true&throwExceptionOnFailure=false"));
from("servlet:///v1.2?matchOnUriPrefix=true")
.recipientList(simple("http4://${header.host}/my-rest-api-v1.2.1?bridgeEndpoint=true&throwExceptionOnFailure=false"));
}
});
UPDATED: I updated the code above according to the next link: http://camel.apache.org/how-do-i-use-dynamic-uri-in-to.html (to use dynamic uri you have to use recipient List EIP).