Using restlet JEE 2.3.2.
I have a client id and secret to interact with the server restful API. Submitting that info gets me back an authorization key that must be used for subsequent request. In curl, I can make queries using that key and can get data back:
curl -XGET "Authorization c79cec57-a52f-4e04-f3ca-55ea2a202114" "https://some/restful/endpoint"
How do I set my client resource to submit that authorization key? The online docs doesn't seem to cover this scenario.
if the scheme is not important, you can use a "Custom" scheme, (as it is mandatory in HTTP specification"). In order to avoid the warning "scheme is not supported by restlet engine", just register one, as follow:
You can achieve what you need using a "custom" scheme, as follow.
// Declare a custom Authenticator helper, if it is not standard
Engine.getInstance().getRegisteredAuthenticators().add(new AuthenticatorHelper(ChallengeScheme.CUSTOM, true, false) {});
// set up the reusable challenge response
ChallengeResponse cred = new ChallengeResponse(ChallengeScheme.CUSTOM);
cred.setRawValue("12344");
ClientResource cr = new ClientResource("http://localhost:8183/");
cr.setChallengeResponse(cred);
cr.get();
If you want an empty scheme, you can do as follow:
ChallengeResponse cred = new ChallengeResponse(new ChallengeScheme("",""));
cred.setRawValue("12345");
In this case, I think that you can use challenge response as described since such feature builds the Authorization header using format Authorization: Scheme ChallengeResponseContent:
ClientResource resource = new ClientResource(resouceURL);
String token = "myToken";
ChallengeResponse cr = new ChallengeResponse(
ChallengeScheme.HTTP_OAUTH_BEARER);
cr.setRawValue(token);
resource.setChallengeResponse(cr);
(...)
As a matter of fact, Restlet requires a challenge scheme that will be added before the token (or something else) within the value of the header Authorization. See extract from class AuthenticatorUtils#formatRequest:
public static String formatRequest(ChallengeRequest challenge,
Response response, Series<Header> httpHeaders) {
String result = null;
if (challenge == null) {
Context.getCurrentLogger().warning(
"No challenge response to format.");
} else if (challenge.getScheme() == null) {
Context.getCurrentLogger().warning(
"A challenge response must have a scheme defined.");
} else if (challenge.getScheme().getTechnicalName() == null) {
Context.getCurrentLogger().warning(
"A challenge scheme must have a technical name defined.");
} else {
ChallengeWriter cw = new ChallengeWriter();
cw.append(challenge.getScheme().getTechnicalName()).appendSpace();
int cwInitialLength = cw.getBuffer().length();
if (challenge.getRawValue() != null) {
cw.append(challenge.getRawValue());
} else {
(...)
In your case, I think that you need to build the header Authorization by yourself as described below:
ClientResource resource = new ClientResource(resouceURL);
String token = "myToken";
resource.getRequest().getHeaders().add("Authorization", token);
resource.get();
You can also implement a custom client resource for your needs in order to automatically apply the token:
public class ProtectedClientResource extends ClientResource {
private String token;
public ProtectedClientResource(String uri) {
super(uri);
}
#Override
public Response handleOutbound(Request request) {
if (token!=null) {
request.getHeaders().add("Authorization", token);
}
return super.handleOutbound(request);
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
Hope it helps you,
Thierry
Related
Developing a Java EE/JSF application, I am trying to include SAML sso functionality into it. Due to technical requirements (SAP BOBJ SDK) I need to use java 8, so I must stick with opensaml 3.x branch. As the application is some years old, I cannot add spring/spring-security to it just for SAML, that's why my code focuses on raw opensaml usage.
Mimicking the example code of this repository, I implemented the authentication basics:
This first code is called when I reach the "login" page. And send the AuthnRequest to my IDP
#Log4j2
#Named
public class SAMLAuthForWPBean implements Serializable {
private static final BasicParserPool PARSER_POOL = new BasicParserPool();
static {
PARSER_POOL.setMaxPoolSize(100);
PARSER_POOL.setCoalescing(true);
PARSER_POOL.setIgnoreComments(true);
PARSER_POOL.setIgnoreElementContentWhitespace(true);
PARSER_POOL.setNamespaceAware(true);
PARSER_POOL.setExpandEntityReferences(false);
PARSER_POOL.setXincludeAware(false);
final Map<String, Boolean> features = new HashMap<>();
features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE);
features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE);
features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE);
features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE);
features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE);
PARSER_POOL.setBuilderFeatures(features);
PARSER_POOL.setBuilderAttributes(new HashMap<>());
}
private String idpEndpoint = "url de azure por";
private String entityId = "glados";
private boolean isLogged;
#Inject
private LoginBean loginBean;
#Inject
private MainBean mainBean;
#Inject
private TechnicalConfigurationBean technicalConfigurationBean;
#PostConstruct
public void init() {
if (!PARSER_POOL.isInitialized()) {
try {
PARSER_POOL.initialize();
} catch (ComponentInitializationException e) {
LOGGER.error("Could not initialize parser pool", e);
}
}
XMLObjectProviderRegistry registry = new XMLObjectProviderRegistry();
ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
registry.setParserPool(PARSER_POOL);
// forge auth endpoint
}
public boolean needLogon() {
return isLogged;
}
public void createRedirection(HttpServletRequest request, HttpServletResponse response)
throws MessageEncodingException,
ComponentInitializationException, ResolverException {
// see this link to build authnrequest with metadata https://blog.samlsecurity.com/2011/01/redirect-with-authnrequest-opensaml2.html
init();
AuthnRequest authnRequest;
authnRequest = OpenSAMLUtils.buildSAMLObject(AuthnRequest.class);
authnRequest.setIssueInstant(DateTime.now());
FilesystemMetadataResolver metadataResolver = new FilesystemMetadataResolver(new File("wp.metadata.xml"));
metadataResolver.setParserPool(PARSER_POOL);
metadataResolver.setRequireValidMetadata(true);
metadataResolver.setId(metadataResolver.getClass().getCanonicalName());
metadataResolver.initialize();
/*
* EntityDescriptor urlDescriptor = metadataResolver.resolveSingle( new CriteriaSet( new BindingCriterion(
* Arrays.asList("urn:oasis:names:tc:SAML:2.0:bindings:metadata"))));
*/
/*entityId = "https://192.168.50.102:8443/360.suite/loginSAML.xhtml";*/
entityId = "glados";
//idp endpoint, je pense => à obtenir des metadata
authnRequest.setDestination(idpEndpoint);
authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI);
// app endpoint
authnRequest.setAssertionConsumerServiceURL("https://192.168.1.14:8443/360.suite/loginSAML.xhtml");
authnRequest.setID(OpenSAMLUtils.generateSecureRandomId());
authnRequest.setIssuer(buildIssuer());
authnRequest.setNameIDPolicy(buildNameIdPolicy());
MessageContext context = new MessageContext();
context.setMessage(authnRequest);
SAMLPeerEntityContext peerEntityContext = context.getSubcontext(SAMLPeerEntityContext.class, true);
SAMLEndpointContext endpointContext = peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);
endpointContext.setEndpoint(URLToEndpoint("https://192.168.1.14:8443/360.suite/loginSAML.xhtml"));
VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty("resource.loader", "classpath");
velocityEngine.setProperty("classpath.resource.loader.class",
"org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
velocityEngine.init();
HTTPPostEncoder encoder = new HTTPPostEncoder();
encoder.setVelocityEngine(velocityEngine);
encoder.setMessageContext(context);
encoder.setHttpServletResponse(response);
encoder.initialize();
encoder.encode();
}
public String doSAMLLogon(HttpServletRequest request, HttpServletResponse response) {
isLogged = true;
technicalConfigurationBean.init();
return loginBean.generateSSOSession(request, technicalConfigurationBean.getSsoPreferences(),
new SamlSSO(technicalConfigurationBean.getCmsPreferences().getCms()));
}
private NameIDPolicy buildNameIdPolicy() {
NameIDPolicy nameIDPolicy = OpenSAMLUtils.buildSAMLObject(NameIDPolicy.class);
nameIDPolicy.setAllowCreate(true);
nameIDPolicy.setFormat(NameIDType.TRANSIENT);
return nameIDPolicy;
}
private Endpoint URLToEndpoint(String URL) {
SingleSignOnService endpoint = OpenSAMLUtils.buildSAMLObject(SingleSignOnService.class);
endpoint.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI);
endpoint.setLocation(URL);
return endpoint;
}
private Issuer buildIssuer() {
Issuer issuer = OpenSAMLUtils.buildSAMLObject(Issuer.class);
issuer.setValue(entityId);
return issuer;
}
}
The redirect is successfully processed and the IDP sends back a POST request to my application that call this code :
#Override
public IEnterpriseSession logon(HttpServletRequest request) throws SDKException, Three60Exception {
HTTPPostDecoder decoder = new HTTPPostDecoder();
decoder.setHttpServletRequest(request);
AuthnRequest authnRequest;
try {
decoder.initialize();
decoder.decode();
MessageContext messageContext = decoder.getMessageContext();
authnRequest = (AuthnRequest) messageContext.getMessage();
OpenSAMLUtils.logSAMLObject(authnRequest);
// Here I Need the user
String user = authnRequest.getSubject().getNameID().getValue();
// BOBJ SDK
String secret = TrustedSso.getSecret();
ISessionMgr sm = CrystalEnterprise.getSessionMgr();
final ITrustedPrincipal trustedPrincipal = sm.createTrustedPrincipal(user, cms, secret);
return sm.logon(trustedPrincipal);
} catch (ComponentInitializationException | MessageDecodingException e) {
return null;
}
}
The issue here is that getSubject() is null on this query.
What did I miss here? Do I need to perform other requests? Do I need to add other configuration in my AuthnRequest?
As stated in the comment, I found the reason why my code was not working.
As I also asked this question on a french forum, can can find the translation of this answer here.
Short answer :
Opensaml knows where to send the authn request thanks to the SAMLPeerEntityContext. In my code I put my own application as the target of this request instead of using the idp HTTP-POST bind endpoint. Once this was changed, everything worked, the idp was answering back the SAMLResponse with proper name.
Long version
On my code, I was building the entity context like this :
SAMLPeerEntityContext peerEntityContext = context.getSubcontext(SAMLPeerEntityContext.class, true);
SAMLEndpointContext endpointContext = peerEntityContext.getSubcontext(SAMLEndpointContext.class, true);
endpointContext.setEndpoint(URLToEndpoint("https://192.168.1.14:8443/360.suite/loginSAML.xhtml"));
This code forces the authn request to be sent to my own application instead of the IDP. As this is the request, it cannot contain the identity.
If I replace this URL by idpEndpoint which I got from the IDP metadata file, the full workflow works as expected.
First something will not work as my IDP forces requests to be signed, so I need to add a signature part.
The "signing and verification" sample of this repository just works for that.
Then, as I need a real identity, I must NOT ask for a transient nameid. In my tests, UNSPECIFIED worked, but PERSISTENT should also make it.
Last, in the ACS receiver, I do NOT receive an authn request but a SAMLResponse with assertions. The code will therefore look like :
String userName =
((ResponseImpl) messageContext.getMessage()).getAssertions().get(0).getSubject().getNameID()
.getValue();
I simplified the code but one, of course, has to check that :
(((ResponseImpl)messageContext.getMessage()).getStatus() is SUCCESS
signatures are valid
assertions are properly populated
Thanks #identigral for your answer in the comment
I'm struggling with invoking GCP cloud functions via REST API using Java.
The steps that I've performed to do it were:
create a service account with role "Cloud Functions Invoker"
download JSON key file for the newly created service account
in my code, obtain an access token using the following method:
private String getAuthToken() {
File credentialsPath = new File(PATH_TO_JSON_KEY_FILE);
GoogleCredentials credentials;
try (FileInputStream serviceAccountStream = new FileInputStream(credentialsPath)) {
credentials = ServiceAccountCredentials.fromStream(serviceAccountStream);
return credentials
.createScoped(Lists.newArrayList("https://www.googleapis.com/auth/cloud-platform"))
.refreshAccessToken()
.getTokenValue();
} catch (IOException e) {
throw new RuntimeException("Action could not be performed");
}
}
perform a REST call, using the created token:
public <Payload, Response> ResponseEntity<Response> callCloudFunction(
String endpoint,
Payload payload,
Class<Response> klazz
) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
headers.setContentType(MediaType.APPLICATION_JSON);
String url = gCloudUrl + endpoint;
String token = getAuthToken();
String payloadString = null;
if (payload != null) {
try {
ObjectMapper objectMapper = new ObjectMapper();
payloadString = objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException e) {
System.out.println(e.getMessage());
throw new RuntimeException("Could not perform action");
}
}
headers.add("Authorization", String.format("Bearer %s", token));
HttpEntity<String> entity = new HttpEntity<>(payloadString, headers);
return restTemplate.exchange(url, HttpMethod.POST, entity, klazz);
}
The implementation looks fine, but in response I'm getting 401 Unauthorized.
Unfortunately, GCP documentation is not really helpful. I think I've searched through all the possible places.
First of all, agree, it's not clear...
Then, you have to know (and it's not clear again) that you need an access token to call Google Cloud API, but and identity token to call IAP (on App Engine for example) or private Cloud Function and Cloud Run. And this identity token need to be signed by Google.
And, as mentioned in the code, you need to have a service account on your computer, but I recommend you to avoid this on GCP, it's not required if you use default authentication (see my code, on your computer set the GOOGLE_APPLICATION_CREDENTIALS env var that points to the service account key file). The best way is to not use service account key file on your computer also, but it's not yet possible (that is a security issue IMO, and I'm discussing with Google on this...)
Anyway, here a code snippet which works in Java (nowhere in the documentation...)
String myUri = "https://path/to/url";
// You can use here your service account key file. But, on GCP you don't require a service account key file.
// However, on your computer, you require one because you need and identity token and you can generate it with your user account (long story... I'm still in discussion with Google about this point...)
Credentials credentials = GoogleCredentials.getApplicationDefault().createScoped("https://www.googleapis.com/auth/cloud-platform");
IdTokenCredentials idTokenCredentials = IdTokenCredentials.newBuilder()
.setIdTokenProvider((IdTokenProvider) credentials)
.setTargetAudience(myUri).build();
HttpRequestFactory factory = new NetHttpTransport().createRequestFactory(new HttpCredentialsAdapter(idTokenCredentials));
HttpRequest request = factory.buildGetRequest(new GenericUrl(myUri));
HttpResponse httpResponse = request.execute();
System.out.println(CharStreams.toString(new InputStreamReader(httpResponse.getContent(), Charsets.UTF_8)));
NOTE If you want to continue to use RestTemplate object and set manually your token, you can generate it like this
String token = ((IdTokenProvider) credentials).idTokenWithAudience(myUri, Collections.EMPTY_LIST).getTokenValue();
System.out.println(token);
I´m currently messing around with JAX-RS specifically Resteasy, because it "just works" with Wildfly and I don´t have to configure anything. That´s really the only reason I use that.
I did already implement Basic Authentication, looking forward to replacing it with OAuth2 later, just did this now for simplicity reasons.
The ContainerRequestFilter looks like this
#Provider
public class SecurityFilter implements ContainerRequestFilter {
private static final String AUTHORIZATION_HEADER_KEY = "Authorization";
private static final String AUTHORIZATION_HEADER_PREFIX = "Basic ";
#Override
public void filter(ContainerRequestContext containerRequestContext) throws IOException {
if(isAuthenticated(containerRequestContext) == false)
containerRequestContext.abortWith(createUnauthorizedResponse("Access denied."));
}
private boolean isAuthenticated(ContainerRequestContext containerRequestContext) {
List<String> authHeader = containerRequestContext.getHeaders().get(AUTHORIZATION_HEADER_KEY);
ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) containerRequestContext.getProperty("org.jboss.resteasy.core.ResourceMethodInvoker");
Method method = methodInvoker.getMethod();
RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
if (authHeader != null && authHeader.size() > 0) {
String authToken = authHeader.get(0).replaceFirst(AUTHORIZATION_HEADER_PREFIX, "");
byte[] decoded = null;
try {
decoded = Base64.getDecoder().decode(authToken);
} catch (IllegalArgumentException ex) {
return false;
}
String decodedString = new String(decoded);
StringTokenizer tokenizer = new StringTokenizer(decodedString, ":");
String username = null, password = null;
if(tokenizer.countTokens() < 2)
return false;
username = tokenizer.nextToken();
password = tokenizer.nextToken();
if (DbController.isValid(username, password, rolesAnnotation.value()))
return true;
}
return false;
}
private Response createUnauthorizedResponse(String msg) {
return Response.status(Response.Status.UNAUTHORIZED)
.entity("{ \"Unauthorized\" : \"" + msg + "\" }")
.type(MediaType.APPLICATION_JSON)
.build();
}
}
It works fine with postman. And I do realize that the main usage of such apis is in well other programs.
But it would be nice, if opened in a browser it would ask you to enter your credentials, instead of just telling you that you are not authorized, with no way to really enter your credentials. Unless you do some trickery to manually put it in the header, but then you might as well just use postman.
If I put a security constraint with auth-constraint role admin it does give a login dialog, but then the authorization does not work and it just keeps asking for authorization.
Is there anything else that I can do instead of containerRequestContext.abortWith? Or do I need to use a completely different approach and it just won´t work with ContainerRequestFilter?
You need to add the WWW-Authenticate header to the response that you abort with. This header tells the browser that it should present the default browser login form.
private static final String CHALLENGE_FORMAT = "%s realm=\"%s\"";
private Response createUnauthorizedResponse() {
return Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, String.format(CHALLENGE_FORMAT, "Basic", "Access"))
.type(MediaType.TEXT_PLAIN_TYPE)
.entity("Credentials are required to access this resource.")
.build();
And here's what the login should look like on Chrome
I'm currently migrating an application from jersey 1 to 2. In the old app, we used a ClientFilter for all jersey clients that automatically refreshed expired OAuth tokens like this:
#Override
public ClientResponse handle(ClientRequest cr) {
ClientResponse resp = getNext().handle(cr);
if (resp.getStatus() == Status.UNAUTHORIZED.getStatusCode()) {
// Try to refresh the token
boolean refreshed = refreshToken(oAuthInfo);
if (refreshed) {
resp = getNext().handle(cr);
}
}
return resp;
}
It might not haven been the most elegant way, but the benefit was that rest client users did not have to care about expired tokens themselves.
With the ContainerResponseFilter for jersey 2, this does not seem to be that simple anymore. The only option I currently see is to use the ClientRequestContext and try to re-create the original request using getClient, getHeaders etc... and then update the result in ContainerResponseContext. This however seems a bit clunky so I was wondering if there is any more convenient way to refresh an OAuth token without having to deal with this wherever a jersey client is used?
It looks like there is not more convenient way than intercepting the response with a client filter, refreshing the token if needed and trying to repeat the exact same request with the new token. In fact, this approach is also used by jersey own filter classes.
Sample code for repeating the original rest call from within a filter class can be found in jerseys HttpAuthenticationFilter:
static boolean repeatRequest(ClientRequestContext request, ClientResponseContext response, String newAuthorizationHeader) {
Client client = request.getClient();
String method = request.getMethod();
MediaType mediaType = request.getMediaType();
URI lUri = request.getUri();
WebTarget resourceTarget = client.target(lUri);
Invocation.Builder builder = resourceTarget.request(mediaType);
MultivaluedMap<String, Object> newHeaders = new MultivaluedHashMap<String, Object>();
for (Map.Entry<String, List<Object>> entry : request.getHeaders().entrySet()) {
if (HttpHeaders.AUTHORIZATION.equals(entry.getKey())) {
continue;
}
newHeaders.put(entry.getKey(), entry.getValue());
}
newHeaders.add(HttpHeaders.AUTHORIZATION, newAuthorizationHeader);
builder.headers(newHeaders);
builder.property(REQUEST_PROPERTY_FILTER_REUSED, "true");
Invocation invocation;
if (request.getEntity() == null) {
invocation = builder.build(method);
} else {
invocation = builder.build(method,
Entity.entity(request.getEntity(), request.getMediaType()));
}
Response nextResponse = invocation.invoke();
if (nextResponse.hasEntity()) {
response.setEntityStream(nextResponse.readEntity(InputStream.class));
}
MultivaluedMap<String, String> headers = response.getHeaders();
headers.clear();
headers.putAll(nextResponse.getStringHeaders());
response.setStatus(nextResponse.getStatus());
return response.getStatus() != Response.Status.UNAUTHORIZED.getStatusCode();
}
This code is used for example in DigestAuthenticator or BasicAuthenticator to repeat a request with provided credentials in case an Unauthorised response is received from server.
I currently have a working web app, but I need to provide means for friend website to consume my data.
There is currently JSON response in place which retrieves some data from my website to caller. It's without authentication currently and I'd like to implement some kind of per request authentication.
My web app has users which are logged in and there is a authentication in place for that. But
I have 3 requests in total for which callers can get data off of my website, what would be the simplest way to add some kind of authentication just for those 3 requests?
I'm using play framework + java
Imo the best options for this would be in the order of simplicity:
Basic authentication (since it's possible to choose either to auth once and then do session-base user recognition or authorize on every request)
2-way SSL
Combination of both
What toolkit do you use for authentication part?
I personally stuck with play-authenticate. So I might be able to answer you question in regard to this toolkit, please apply it to your particular toolkit as needed.
I will provide Basic authentication example as the easiest one. The benefit is: you could start with it and add on top it later (e.g. add Client certificate authentication via Apache later on).
So, my controller code snippet
#Restrict(value = #Group({"ROLE_WEB_SERVICE1"}), handler = BasicAuthHandler.class)
public static Result ws1() {
return TODO;
}
And the authentification handler itself
public class BasicAuthHandler extends AbstractDeadboltHandler {
public static final String HEADER_PREFIX = "Basic ";
private static final String AUTHORIZATION = "authorization";
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
#Override
public Result beforeAuthCheck(final Http.Context context) {
return basicAuthenticate(context);
}
private Result basicAuthenticate(Http.Context context) {
if (PlayAuthenticate.isLoggedIn(context.session())) {
// user is logged in
return null;
}
final String authHeader = context.request().getHeader(AUTHORIZATION);
if (authHeader == null || !authHeader.toLowerCase().startsWith(HEADER_PREFIX.toLowerCase())) {
return onAuthFailure(context, "Basic authentication header is missing");
}
final String auth = authHeader.substring(HEADER_PREFIX.length());
final byte[] decodedAuth;
final String[] credentials;
try {
decodedAuth = Base64.base64ToByteArray(auth);
credentials = new String(decodedAuth, "UTF-8").split(":");
} catch (final IOException e) {
Logger.error("basicAuthenticate", e);
return Results.internalServerError();
}
if (credentials.length != 2) {
return onAuthFailure(context, "Could not authenticate with absent password");
}
final String username = credentials[0];
final String password = credentials[1];
final AuthUser authUser = new AuthUser(password, username);
final Enum result = AuthProvider.getProvider().loginUser(authUser);
if ("USER_LOGGED_IN".equals(result.name())) {
PlayAuthenticate.storeUser(context.session(), authUser);
return null;
}
return onAuthFailure(context, "Authenticate failure");
}
#Override
public Subject getSubject(final Http.Context context) {
// your implementation
}
#Override
public Result onAuthFailure(final Http.Context context,
final String content) {
// your error hangling logic
return super.onAuthFailure(context, content);
}
}
Hopefully it fills in some blanks