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
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
We are trying to connect to webservice (from Java) that has ADFS SAML authentication.
All the examples I have seen, use Basic Authentication over HTTPS. (I am just using HttpsURLConnection to make a request for now, not using anything like Axis or JAX-WS)
I am not sure how to approach ADFS SAML authentication. Here's what I understand so far (don't know much about SAML):
I make one request, pass username/password and get the
authentication token back
Save the authentication token
Pass the token as some SOAP attribute in my calls where I invoke an
actual operation on the webservice
No idea under which attribute would I put this authentication token though
Is my above approach correct? If so, is there some library that I can use that does all this?
If not how can I go about doing this manually?
Please let me know if there are other or better ways of going about this.
If you are trying to build native app then can use below code. i has tried to use power bi rest apis. once you gets token you can use that in api calls.
public class PublicClient {
private final static String AUTHORITY = "https://login.microsoftonline.com/common";
private final static String CLIENT_ID = "XXXX-xxxx-xxx-xxx-xxxX";
private final static String RESOURCE = "https://analysis.windows.net/powerbi/api";
public static void main(String args[]) throws Exception {
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
System.out.print("Enter username: ");
String username = br.readLine();
System.out.print("Enter password: ");
String password = br.readLine();
AuthenticationResult result = getAccessTokenFromUserCredentials(
username, password);
System.out.println("Access Token - " + result.getAccessToken());
System.out.println("Refresh Token - " + result.getRefreshToken());
System.out.println("ID Token Expires on - " + result.getExpiresOn());
}
}
private static AuthenticationResult getAccessTokenFromUserCredentials(
String username, String password) throws Exception {
AuthenticationContext context = null;
AuthenticationResult result = null;
ExecutorService service = null;
try {
service = Executors.newFixedThreadPool(1);
context = new AuthenticationContext(AUTHORITY, false, service);
Future<AuthenticationResult> future = context.acquireToken(
RESOURCE, CLIENT_ID, username, password, null);
result = future.get();
} finally {
service.shutdown();
}
if (result == null) {
throw new ServiceUnavailableException(
"authentication result was null");
}
return result;
}
}
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
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
I am trying to write a server side Facebook Notification service in my GWT app. The idea is that I will run this as a timertask or cron job sort of.
With the code below, I get a login URL, I want to be able to Login programmatically as this is intended to be automated (Headless sort of way). I was gonna try do a submit with HTMLunit but I thought the FB API should cater for this.
Please advice.
public class NotificationServiceImpl extends RemoteServiceServlet implements NotificationService {
/**serialVersionUID*/
private static final long serialVersionUID = 6893572879522128833L;
private static final String FACEBOOK_USER_CLIENT = "facebook.user.client";
long facebookUserID;
public String sendMessage(Notification notification) throws IOException {
String api_key = notification.getApi_key();
String secret = notification.getSecret_key();
try {
// MDC.put(ipAddress, req.getRemoteAddr());
HttpServletRequest request = getThreadLocalRequest();
HttpServletResponse response = getThreadLocalResponse();
HttpSession session = getThreadLocalRequest().getSession(true);
// session.setAttribute("api_key", api_key);
IFacebookRestClient<Document> userClient = getUserClient(session);
if(userClient == null) {
System.out.println("User session doesn't have a Facebook API client setup yet. Creating one and storing it in the user's session.");
userClient = new FacebookXmlRestClient(api_key, secret);
session.setAttribute(FACEBOOK_USER_CLIENT, userClient);
}
System.out.println("Creating a FacebookWebappHelper, which copies fb_ request param data into the userClient");
FacebookWebappHelper<Document> facebook = new FacebookWebappHelper<Document>(request, response, api_key, secret, userClient);
String nextPage = request.getRequestURI();
nextPage = nextPage.substring(nextPage.indexOf("/", 1) + 1); //cut out the first /, the context path and the 2nd /
System.out.println(nextPage);
boolean redirectOccurred = facebook.requireLogin(nextPage);
if(redirectOccurred) {
return null;
}
redirectOccurred = facebook.requireFrame(nextPage);
if(redirectOccurred) {
return null;
}
try {
facebookUserID = userClient.users_getLoggedInUser();
if (userClient.users_hasAppPermission(Permission.STATUS_UPDATE)) {
userClient.users_setStatus("Im testing Facebook With Java! This status is written using my Java code! Can you see it? Cool :D", false);
}
} catch(FacebookException ex) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error while fetching user's facebook ID");
System.out.println("Error while getting cached (supplied by request params) value " +
"of the user's facebook ID or while fetching it from the Facebook service " +
"if the cached value was not present for some reason. Cached value = {}" + userClient.getCacheUserId());
return null;
}
// MDC.put(facebookUserId, String.valueOf(facebookUserID));
// chain.doFilter(request, response);
} finally {
// MDC.remove(ipAddress);
// MDC.remove(facebookUserId);
}
return String.valueOf(facebookUserID);
}
public static FacebookXmlRestClient getUserClient(HttpSession session) {
return (FacebookXmlRestClient)session.getAttribute(FACEBOOK_USER_CLIENT);
}
}
Error message:
[ERROR] com.google.gwt.user.client.rpc.InvocationException: <script type="text/javascript">
[ERROR] top.location.href = "http://www.facebook.com/login.php?v=1.0&api_key=MY_KEY&next=notification";
[ERROR] </script>