I am looking some way to make some authentication for my play framework app: I want allow/disallow the whole access to non authenticated users
Is there exists some working module/solution for it? I don't need any forms for auth, just 401 HTTP response for non authenticated users (like Apache .htacccess "AuthType Basic" mode).
I've updated Jonck van der Kogel's answer to be more strict in parsing the authorization header, to not fail with ugly exceptions if the auth header is invalid, to allow passwords with ':', and to work with Play 2.6:
So, BasicAuthAction class:
import java.io.UnsupportedEncodingException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import org.apache.commons.codec.binary.Base64;
import play.Logger;
import play.Logger.ALogger;
import play.mvc.Action;
import play.mvc.Http;
import play.mvc.Http.Context;
import play.mvc.Result;
public class BasicAuthAction extends Action<Result> {
private static ALogger log = Logger.of(BasicAuthAction.class);
private static final String AUTHORIZATION = "Authorization";
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
private static final String REALM = "Basic realm=\"Realm\"";
#Override
public CompletionStage<Result> call(Context context) {
String authHeader = context.request().getHeader(AUTHORIZATION);
if (authHeader == null) {
context.response().setHeader(WWW_AUTHENTICATE, REALM);
return CompletableFuture.completedFuture(status(Http.Status.UNAUTHORIZED, "Needs authorization"));
}
String[] credentials;
try {
credentials = parseAuthHeader(authHeader);
} catch (Exception e) {
log.warn("Cannot parse basic auth info", e);
return CompletableFuture.completedFuture(status(Http.Status.FORBIDDEN, "Invalid auth header"));
}
String username = credentials[0];
String password = credentials[1];
boolean loginCorrect = checkLogin(username, password);
if (!loginCorrect) {
log.warn("Incorrect basic auth login, username=" + username);
return CompletableFuture.completedFuture(status(Http.Status.FORBIDDEN, "Forbidden"));
} else {
context.request().setUsername(username);
log.info("Successful basic auth login, username=" + username);
return delegate.call(context);
}
}
private String[] parseAuthHeader(String authHeader) throws UnsupportedEncodingException {
if (!authHeader.startsWith("Basic ")) {
throw new IllegalArgumentException("Invalid Authorization header");
}
String[] credString;
String auth = authHeader.substring(6);
byte[] decodedAuth = new Base64().decode(auth);
credString = new String(decodedAuth, "UTF-8").split(":", 2);
if (credString.length != 2) {
throw new IllegalArgumentException("Invalid Authorization header");
}
return credString;
}
private boolean checkLogin(String username, String password) {
/// change this
return username.equals("vlad");
}
}
And then, in controller classes:
#With(BasicAuthAction.class)
public Result authPage() {
String username = request().username();
return Result.ok("Successful login as user: " + username + "! Here's your data: ...");
}
You can try this filter:
https://github.com/Kaliber/play-basic-authentication-filter
It looks pretty simple to use and configure.
You could also solve this with a play.mvc.Action, like this.
First your Action:
import org.apache.commons.codec.binary.Base64;
import play.libs.F;
import play.libs.F.Promise;
import play.mvc.Action;
import play.mvc.Http.Context;
import play.mvc.Result;
import util.ADUtil;
public class BasicAuthAction extends Action<Result> {
private static final String AUTHORIZATION = "authorization";
private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
private static final String REALM = "Basic realm=\"yourRealm\"";
#Override
public Promise<Result> call(Context context) throws Throwable {
String authHeader = context.request().getHeader(AUTHORIZATION);
if (authHeader == null) {
context.response().setHeader(WWW_AUTHENTICATE, REALM);
return F.Promise.promise(new F.Function0<Result>() {
#Override
public Result apply() throws Throwable {
return unauthorized("Not authorised to perform action");
}
});
}
String auth = authHeader.substring(6);
byte[] decodedAuth = new Base64().decode(auth);
String[] credString = new String(decodedAuth, "UTF-8").split(":");
String username = credString[0];
String password = credString[1];
// here I authenticate against AD, replace by your own authentication mechanism
boolean loginCorrect = ADUtil.loginCorrect(username, password);
if (!loginCorrect) {
return F.Promise.promise(new F.Function0<Result>() {
#Override
public Result apply() throws Throwable {
return unauthorized("Not authorised to perform action");
}
});
} else {
return delegate.call(context);
}
}
}
Next your annotation:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import play.mvc.With;
#With(BasicAuthAction.class)
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.METHOD, ElementType.TYPE})
#Inherited
#Documented
public #interface BasicAuth {
}
You can now annotate your controller functions as follows:
#BasicAuth
public Promise<Result> yourControllerFunction() {
...
I'm afraid there's no such solution, reason is simple: usually when devs need to add authorization/authentication stack they build full solution.
The easiest and fastest way is using HTTP front-end server as a reverse-proxy for your application (I'd choose nginx for that task, but if you have running Apache on the machine it can be used as well). It will allow you to filter/authenticate the traffic with common server's rules
Additionally it gives you other benefits, i.e.: you can create CDN-like path, so you won't waste your apps' resources for serving public, static assets. You can use load-balancer for redeploying your app without stopping it totally for x minutes, etc.
Related
I'm trying to find a way to change the "sub" format in JWT Token provided by Keycloak, I know it came from Keycloak User Id but i'm not sure we can't change it.
For example for now I have something like this :
"sub": "f:39989175-b393-4fad-8f84-628b9712f93b:testldap",
I would like it smaller 😅.
I'm not sure that modifying 'sub' is a good idea, but if you sure, you can use something like that:
/**
* Class for signing JWT (when you get tokens in base64 actually they are
* signed by issuer server see https://jwt.io)
*/
public static class JwtSigner {
private final KeyPair keyPair;
private final String kid;
public JwtSigner(String privateKeyPem) {
PrivateKey privateKey = PemUtils.decodePrivateKey(privateKeyPem);
PublicKey publicKey = KeyUtils.extractPublicKey(privateKey);
keyPair = new KeyPair(publicKey, privateKey);
kid = KeyUtils.createKeyId(keyPair.getPublic());
}
public String encodeToken(AccessToken accessToken) {
return new JWSBuilder()
.type("JWT")
.kid(kid)
.jsonContent(accessToken)
.sign(Algorithm.RS256, keyPair.getPrivate());
}
}
/**
* This class allows you to update several token fields and re-encode token
*/
public static class JwtTransformer<T extends AccessToken> {
private T token;
public JwtTransformer(String tokenString, Class<T> tokenType) throws JWSInputException {
try {
token = JsonSerialization.readValue(new JWSInput(tokenString).getContent(), tokenType);
} catch (IOException e) {
throw new JWSInputException(e);
}
}
public static <T extends AccessToken> T decode(String tokenString, Class<T> tokenType) throws JWSInputException {
return new JwtTransformer<>(tokenString, tokenType).decode();
}
public static JwtTransformer<AccessToken> forAccessToken(String tokenString) throws JWSInputException {
return new JwtTransformer<>(tokenString, AccessToken.class);
}
public static JwtTransformer<RefreshToken> forRefreshToken(String tokenString) throws JWSInputException {
return new JwtTransformer<>(tokenString, RefreshToken.class);
}
public T decode() {
return token;
}
public JwtTransformer transform(Consumer<T> consumer) {
consumer.accept(token);
return this;
}
public String encode(JwtSigner jwtSigner) {
return jwtSigner.encodeToken(token);
}
}
I used this classes for tests, but you can adopt them for your needs. Take a note that private key that required for JwtSigner initializaton is stored in keycloak DB, and can not be easily extracted via Admin Console UI. Check out result of
select VALUE
from KEYCLOAK.COMPONENT
inner join KEYCLOAK.COMPONENT_CONFIG
on KEYCLOAK.COMPONENT.ID = KEYCLOAK.COMPONENT_CONFIG.COMPONENT_ID
where PARENT_ID = '%YOUR_REALM_NAME%'
and PROVIDER_ID = 'rsa-generated'
and COMPONENT_CONFIG.NAME = 'privateKey';
So finally you can do something like
String new AccessToken = JwtTransformer.forAccessToken(accessTokenString)
.transform(token -> {
token.subject(subModificationFunction(token.getSubject()))
})
.encode();
I have a very basic Vertx demo I'm trying to create that fetches a JWK from an endpoint and creates an RSAPublicKey for verifying a JWT signature:
package example;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpServer;
import io.vertx.ext.web.Router;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import com.auth0.jwt.interfaces.RSAKeyProvider;
import java.security.interfaces.RSAPrivateKey;
public class MainVerticle extends AbstractVerticle {
#Override
public void start(Promise<Void> startPromise) throws Exception {
HttpServer server = vertx.createHttpServer();
Router router = Router.router(vertx);
router.route().handler(routingContext -> {
String authHeader = routingContext.request().getHeader("Authorization");
// pull token from header
String token = authHeader.split(" ")[1];
URL jwksEndpoint = null;
try {
jwksEndpoint = new URL("http://localhost:1080/jwks");
} catch (MalformedURLException e) {
e.printStackTrace();
}
JwkProvider jwkProvider = new UrlJwkProvider(jwksEndpoint);
RSAKeyProvider keyProvider = new RSAKeyProvider() {
#Override
public RSAPublicKey getPublicKeyById(String kid) {
//Received 'kid' value might be null if it wasn't defined in the Token's header
RSAPublicKey publicKey = null;
try {
publicKey = (RSAPublicKey) jwkProvider.get(kid).getPublicKey();
} catch (JwkException e) {
e.printStackTrace();
}
return publicKey;
}
#Override
public RSAPrivateKey getPrivateKey() {
return null;
}
#Override
public String getPrivateKeyId() {
return null;
}
};
Algorithm algorithm = Algorithm.RSA256(keyProvider);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth0")
.build();
DecodedJWT jwt = verifier.verify(token);
System.out.println(jwt);
routingContext.next();
});
router.route("/hello").handler(ctx -> {
ctx.response()
.putHeader("content-type", "text/html")
.end("<h1>Hello from non-clustered messenger example!</h1>");
});
server.requestHandler(router).listen(8888, http -> {
if(http.succeeded()) {
startPromise.complete();
System.out.println("HTTP server started on port 8888");
} else {
startPromise.fail(http.cause());
}
});
}
}
The problem is that when I make request to the /hello endpoint, the application immediately returns a 500. But nothing appears in the logs (even at debug level).
I've tried manually specifying the kid property to rule out the jwkProvider not returning properly
I'm at a loss at how to gain any more insight into what is failing.
Turns out to completely be my oversight. Wrapping that verifier.verify() call in a try/catch showed me that I was expecting an issuer. This is the same problem I was having while trying to achieve this in Quarkus! I was able to remove that from the builder and now this works perfectly.
I am trying to use Zoom API which requires using JWT. I have successfully using node.js to generate JWT and call the Zoom API like this:
var jwt = require('jsonwebtoken');
var zoom_key = "abcd";
var zoom_secret = "efgh";
var payload = {
iss: Zoom_Key,
exp: ((new Date()).getTime() + 3600)
};
//Automatically creates header, and returns JWT
var token = jwt.sign(payload, zoom_secret);
module.exports = token;
However, right now I am trying to use the same way in JAVA. When I using the token by JJWT, it always give the the "invalid token" error message from zoom api. Can any one help me figure out the reason?
First, I create a Playload class in Playload.java like this:
// in Playload.java
package palyload;
public class Playload {
public String iss;
public Playload(String iss) {
this.iss = iss;
}
}
Second,
I import JJWT and play.libs.Json to generate JWT token like this:
package controllers;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import play.libs.Json;
import play.mvc.Controller;
import play.mvc.Result;
import palyload.Playload;
public class JWT extends Controller {
private static final String zoom_key = "abcd";
private static final String zoom_secret = "efgh";
private static final String baseUrl = "https://api.zoom.us/v2/users/?access_token=";
public String setToken (Object obj) {
String token = "";
Map<String, Object> map = new HashMap<>();
map.put("json", Json.toJson(obj));
try {
token = Jwts.builder()
.setClaims(map)
.setExpiration(Date.from(ZonedDateTime.now(ZoneId.systemDefault()).plusSeconds(3600).toInstant()))
.signWith(SignatureAlgorithm.HS256, zoom_secret)
.compact();
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
System.out.println(Jwts.parser().setSigningKey(zoom_secret).parseClaimsJws(token).getBody().getSubject().toString());
// System.out.println(map.toString());
return token;
}
public String getToken () {
Playload pl = new Playload(zoom_key);
System.out.println("playload: " + Json.toJson(pl).toString());
return setToken(pl);
}
public String setUrl () {
return baseUrl + getToken();
}
public Result getUrl () {
return ok(setUrl());
}
}
Have no idea why this token url is not correct like Node.js one?
I am trying to extract data from the Samples namespace that comes with Intersystems Cache install. Specifically, I am trying to retrieve Sample.Company global data using XEP. Inorder to achieve this, I created Sample.Company class like this -
package Sample;
public class Company {
public Long id;
public String mission;
public String name;
public Long revenue;
public String taxId;
public Company(Long id, String mission, String name, Long revenue,
String taxId) {
this.id = id;
this.mission = mission;
this.name = name;
this.revenue = revenue;
this.taxId = taxId;
}
public Company() {
}
}
XEP related code looks like this -
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import Sample.Company;
import com.intersys.xep.Event;
import com.intersys.xep.EventPersister;
import com.intersys.xep.EventQuery;
import com.intersys.xep.EventQueryIterator;
import com.intersys.xep.PersisterFactory;
import com.intersys.xep.XEPException;
#Service
public class CompanyService {
public List<Company> fetch() {
EventPersister myPersister = PersisterFactory.createPersister();
myPersister.connect("SAMPLES", "user", "pwd");
try { // delete any existing SingleStringSample events, then import
// new ones
Event.isEvent("Sample.Company");
myPersister.deleteExtent("Sample.Company");
String[] generatedClasses = myPersister.importSchema("Sample.Company");
for (int i = 0; i < generatedClasses.length; i++) {
System.out.println("Event class " + generatedClasses[i]
+ " successfully imported.");
}
} catch (XEPException e) {
System.out.println("import failed:\n" + e);
throw new RuntimeException(e);
}
EventQuery<Company> myQuery = null;
List<Company> list = new ArrayList<Company>();
try {
Event newEvent = myPersister.getEvent("Sample.Company");
String sql = "Select * from Sample.Company";
myQuery = newEvent.createQuery(sql);
newEvent.close();
myQuery.execute();
EventQueryIterator<Company> iterator = myQuery.getIterator();
while (iterator.hasNext()) {
Company c = iterator.next();
System.out.println(c);
list.add(c);
}
myQuery.close();
myPersister.close();
return list;
} catch (XEPException e) {
System.out.println("createQuery failed:\n" + e);
throw new RuntimeException(e);
}
}
}
When I try executing the fetch() method of the above class, I am seeing the following exception -
com.intersys.xep.XEPException: Cannot import - extent for Sample.Company not empty.
at com.intersys.xep.internal.Generator.generate(Generator.java:52)
at com.intersys.xep.EventPersister.importSchema(EventPersister.java:954)
at com.intersys.xep.EventPersister.importSchema(EventPersister.java:363)
I got the simple string example working. Does it mean, we can not read the existing data using XEP? If we can read, Could some please help me in resolving the above issue? Thanks in advance.
You are trying to create a new class named Sample.Company in your instance:
String[] generatedClasses = myPersister.importSchema("Sample.Company");
But you still have data and an existing class there.
Im trying to view a https url in a webview that requires ssl authentication.
im having a similar problem to this post :
How to handle Basic Authentication in WebView
where i get a 401 unauthorised error.
I dont want users to enter a user name or password as im doing my authentication with certificates.
i have got the client certificate in 2 ways, as a X509Certificate using keystore and as a bks keystore.
can anyone help me with how am i supposed to tell the webview to use this certificate when loading the url.
Code is from https://github.com/potaka001/WebViewBasicAuthTest
Of course, you are interested in the onReceivedHttpAuthRequest method.
package com.webviewbasicauthtest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.webkit.CookieManager;
import android.webkit.HttpAuthHandler;
import android.webkit.SslErrorHandler;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
public class MyWebViewClient extends WebViewClient {
private String loginCookie;
private Context mContext;
private WebView mWebView;
public MyWebViewClient(Context context, WebView webview) {
super();
mContext = context;
mWebView = webview;
}
#Override
public void onPageStarted( WebView view, String url, Bitmap favicon ) {
}
#Override
public void onPageFinished( WebView view, String url ) {
CookieManager cookieManager = CookieManager.getInstance();
cookieManager.setCookie(url, loginCookie);
}
#Override
public void onReceivedError( WebView view, int errorCode, String description, String failingUrl ) {
Toast.makeText(view.getContext(), "ページ読み込みエラー", Toast.LENGTH_LONG).show();
}
#Override
public void onLoadResource( WebView view, String url ){
CookieManager cookieManager = CookieManager.getInstance();
loginCookie = cookieManager.getCookie(url);
}
#Override
public boolean shouldOverrideUrlLoading( WebView view, String url ) {
return false;
}
#Override
public void onReceivedSslError( WebView view, SslErrorHandler handler, SslError error ) {
handler.proceed();
}
#Override
public void onReceivedHttpAuthRequest( WebView view, final HttpAuthHandler handler, final String host, final String realm ){
String userName = null;
String userPass = null;
if (handler.useHttpAuthUsernamePassword() && view != null) {
String[] haup = view.getHttpAuthUsernamePassword(host, realm);
if (haup != null && haup.length == 2) {
userName = haup[0];
userPass = haup[1];
}
}
if (userName != null && userPass != null) {
handler.proceed(userName, userPass);
}
else {
showHttpAuthDialog(handler, host, realm, null, null, null);
}
}
private void showHttpAuthDialog( final HttpAuthHandler handler, final String host, final String realm, final String title, final String name, final String password ) {
LinearLayout llayout = new LinearLayout((Activity)mContext);
final TextView textview1 = new TextView((Activity)mContext);
final EditText edittext1 = new EditText((Activity)mContext);
final TextView textview2 = new TextView((Activity)mContext);
final EditText edittext2 = new EditText((Activity)mContext);
llayout.setOrientation(LinearLayout.VERTICAL);
textview1.setText("username:");
textview2.setText("password:");
llayout.addView(textview1);
llayout.addView(edittext1);
llayout.addView(textview2);
llayout.addView(edittext2);
final Builder mHttpAuthDialog = new AlertDialog.Builder((Activity)mContext);
mHttpAuthDialog.setTitle("Basic Authentication")
.setView(llayout)
.setCancelable(false)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
EditText etUserName = edittext1;
String userName = etUserName.getText().toString();
EditText etUserPass = edittext2;
String userPass = etUserPass.getText().toString();
mWebView.setHttpAuthUsernamePassword(host, realm, name, password);
handler.proceed(userName, userPass);
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
handler.cancel();
}
})
.create().show();
}
}