I'm trying to use Java SignPost to get Oauth support from discogs
If I call this method it provides a url to a website, then if I login I get a verification code, so far so good.
public static String requestDiscogsAuthorization() throws Exception
{
OAuthConsumer consumer = new DefaultOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
OAuthProvider provider = new DefaultOAuthProvider(
"http://api.discogs.com/oauth/request_token",
"http://api.discogs.com/oauth/access_token",
"http://www.discogs.com/oauth/authorize");
provider.setRequestHeader("User-Agent", SongKong.USER_AGENT);
return provider.retrieveRequestToken(consumer, OAuth.OUT_OF_BAND);
}
I store this verification code in my application and can retrieve as UserPreferences.getInstance().getDiscogsAuthorization(). So now I want to sign a url I try and call the getAccessToken() below to get an access code from my verification key but I get
oauth.signpost.exception.OAuthExpectationFailedException: Authorized request token or token secret not set. Did you retrieve an authorized request token before?
at oauth.signpost.AbstractOAuthProvider.retrieveAccessToken(AbstractOAuthProvider.java:89)
at com.jthink.songkong.analyse.toplevelanalyzer.DiscogsAuth.getAccessToken(DiscogsAuth.java:54)
I dont see what im doing wrong
public class DiscogsAuth
{
/**
* Constructs a consumer with valid access token for signing urls (can only be used once user has authorizaed application and auth code
* available to application somewhere
*
* NOTE:Not thread safe, has to be done once for each thread
*
* #return
* #throws Exception
*/
public static OAuthConsumer getAccessToken() throws Exception
{
OAuthConsumer consumer = new DefaultOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
OAuthProvider provider = new DefaultOAuthProvider(
"http://api.discogs.com/oauth/request_token",
"http://api.discogs.com/oauth/access_token",
"http://www.discogs.com/oauth/authorize");
provider.setRequestHeader("User-Agent", SongKong.USER_AGENT);
provider.retrieveAccessToken(consumer, UserPreferences.getInstance().getDiscogsAuthorization());
return consumer;
}
/**
* Sign Url Connection
*
* #param urlConnection
* #param consumer
* #throws Exception
*/
public static void signUrl(HttpURLConnection urlConnection, OAuthConsumer consumer) throws Exception
{
consumer.sign(urlConnection);
}
}
Probably your method call not able to get the verification code when you try to call provider.retrieveAccessToken ,you can try by printing the verification code before you pass it on to the provider.retrieveAccessToken method.
You are missing access token and access token secret.
To sign an OAuth 1.0 request you need three things: the CONSUMER_SECRET that you already have, the access token and the access token secret which you will get in the oauth_token and oauth_token_secret variables respectively in the response of the Temporary Credentials request.
I have no experience with Java, but according to Signpost documentation i guess that you should call this...
consumer.setTokenWithSecret(ACCESS_TOKEN, TOKEN_SECRET);
...before you try to sign the url
consumer.sign(urlConnection);
Seems like you can get ACCESS_TOKEN and TOKEN_SECRET with ...
String accessToken = provider.getResponseParameter('oauth_token');
String tokenSecret = provider.getResponseParameter('oauth_token_secret');
... after you call this:
provider.retrieveAccessToken(consumer, verificationCode);
Here is my take (I didn't try it):
public class DiscogsAuth
{
/**
* Constructs a consumer with valid access token for signing urls (can only be used once user has authorizaed application and auth code
* available to application somewhere
*
* NOTE:Not thread safe, has to be done once for each thread
*
* #return
* #throws Exception
*/
public static OAuthConsumer getAccessToken() throws Exception
{
OAuthConsumer consumer = new DefaultOAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET);
OAuthProvider provider = new DefaultOAuthProvider(
"http://api.discogs.com/oauth/request_token",
"http://api.discogs.com/oauth/access_token",
"http://www.discogs.com/oauth/authorize");
provider.setRequestHeader("User-Agent", SongKong.USER_AGENT);
provider.retrieveAccessToken(consumer, UserPreferences.getInstance().getDiscogsAuthorization());
// Retrieve access token and access token secret
String accessToken = provider.getResponseParameter('oauth_token');
String tokenSecret = provider.getResponseParameter('oauth_token_secret');
// Set them on the consumer so it can sign the requestst with them
consumer.setTokenWithSecret(accessToken, tokenSecret);
return consumer;
}
/**
* Sign Url Connection
*
* #param urlConnection
* #param consumer
* #throws Exception
*/
public static void signUrl(HttpURLConnection urlConnection, OAuthConsumer consumer) throws Exception
{
consumer.sign(urlConnection);
}
}
The problem was that I was not using the same instance of OAuthConsumer and OAuthProvider for every step.
Related
I'm writing an application using Spring Boot and Java that will be writing files to Azure Blob Storage. How can I use a Service Principal to authenticate? The details of the SP should ideally be read in via some properties or an external file.
I've been wading through the reams of documentation and examples, all of which don't seem to be quite what I'm looking for. Most examples that I've seen use the Storage Account Key which I don't want to do.
Some example code would be really appreciated. As I said, I'm struggling to find a decent example (both of how to use an SP but also generally how to write to Azure BLOB Storage in Java) as there seems to be so many different ways of accessing storage scattered around in the microsoft docs.
You can use ADAL4J to acquire a token, and then use the token to write to blobs.
Add role assignment to your principal.
Get token.
public static String getToken() throws Exception {
String TENANT_ID = "your tenant id or name, e4c9*-*-*-*-*57fb";
String AUTHORITY = "https://login.microsoftonline.com/" + TENANT_ID;
String CLIENT_ID = "your application id, dc17*-*-*-*a5e7";
String CLIENT_SECRET = "the secret, /pG*32";
String RESOURCE = "https://storage.azure.com/";
String ACCESS_TOKEN = null;
ExecutorService service = Executors.newFixedThreadPool(1);
AuthenticationContext context = null;
try {
context = new AuthenticationContext(AUTHORITY, false, service);
ClientCredential credential = new ClientCredential(CLIENT_ID, CLIENT_SECRET);
Future<AuthenticationResult> future = context.acquireToken(RESOURCE, credential, null);
ACCESS_TOKEN = future.get().getAccessToken();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} finally {
service.shutdown();
}
return ACCESS_TOKEN;
}
Access blob.
public static void main(String[] args) throws Exception {
String token = getToken();
StorageCredentialsToken credentialsToken = new StorageCredentialsToken("storagetest789", token);
CloudBlobClient blobClient = new CloudBlobClient(new URI("https://storagetest789.blob.core.windows.net/"), credentialsToken);
CloudBlobContainer blobContainer = blobClient.getContainerReference("pub");
CloudBlockBlob blockBlob = blobContainer.getBlockBlobReference("test.txt");
blockBlob.uploadText("Test!");
}
Hope it helps.
I have created an article on connecting Spring Boot App with Azure Storage account using Serivce Principal you can refer that.
https://medium.com/#iamdeveshkumar/using-azure-blob-storage-with-a-spring-boot-app-6238c137df7
pom.xml
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-storage-blob</artifactId>
<version>12.12.0</version>
</dependency>
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.3.1</version>
</dependency>
application.properties
app.config.azure.client-id=xxxxxxxxxxx
app.config.azure.client-secret=xxxxxxxxxxx
app.config.azure.tenant-id=xxxxxxxxxxx
app.config.azure.storage-id=xxxxxxxxxxx
app.config.azure.storage-endpoint=https://{STORAGE-ID}.blob.core.windows.net
app.config.azure.storage.container=xxxxxxxxxxx
AzureStorageConfiguration.java
#Data
#Configuration
#Slf4j
public class AzureStorageConfiguration {
private static final Logger logger = LoggerFactory.getLogger(AzureStorageConfiguration.class);
#Value("${app.config.azure.client-id}")
private String clientId;
#Value("${app.config.azure.client-secret}")
private String clientSecret;
#Value("${app.config.azure.tenant-id}")
private String tenantId;
#Value("${app.config.azure.storage-id}")
private String storageId;
#Value("${app.config.azure.storage-endpoint}")
private String storageEndpoint;
#Value("${app.config.azure.storage.container}")
private String storageContainer;
/**
* Blob service client builder blob service client builder.
*
* #return the blob service client builder
*/
#Bean
public BlobServiceClientBuilder blobServiceClientBuilder() {
return new BlobServiceClientBuilder()
.credential(getAzureClientCredentials())
.endpoint(getStorageEndpoint());
}
private ClientSecretCredential getAzureClientCredentials() {
return new ClientSecretCredentialBuilder()
.clientId(clientId)
.clientSecret(clientSecret)
.tenantId(tenantId)
.build();
}
/**
* Gets storage endpoint.
*
* #return the storage endpoint
*/
public String getStorageEndpoint() {
return storageEndpoint.replace("{STORAGE-ID}", storageId);
}
/**
* A util method to upload a file to Azure Storage.
*
* #param blobServiceClientBuilder service client builder
* #return BlobServiceAsyncClient blob service async client
*/
#Bean(name = "blobServiceAsyncClient")
public BlobServiceAsyncClient blobServiceAsyncClient(
BlobServiceClientBuilder blobServiceClientBuilder) {
/*
retryDelay is by default 4ms and maxRetryDelay is by default 120ms
*/
return blobServiceClientBuilder.retryOptions(
new RequestRetryOptions(
RetryPolicyType.EXPONENTIAL,
5,
Duration.ofSeconds(300L),
null,
null,
null)).buildAsyncClient();
}
}
Then you can use the BlobServiceAsyncClient to create BlobAsyncClient for various blob operations.
/**
* Get blob async client blob async client.
*
* #param container the container
* #param blobName the blob name
* #return the blob async client
*/
public BlobAsyncClient getBlobAsyncClient(String container, String blobName) {
BlobContainerAsyncClient blobContainerAsyncClient =
blobServiceAsyncClient.getBlobContainerAsyncClient(container);
return blobContainerAsyncClient.getBlobAsyncClient(blobName);
}
/**
* Upload to azure blob.
*
* #param container the container
* #param blobName the blob name
* #param data the data
*/
public void uploadToAzureBlob(String container, String blobName, byte[] data) {
BlobAsyncClient blobAsyncClient = getBlobAsyncClient(container, blobName);
long blockSize = 2L * 1024L * 1024L; //2MB
blobAsyncClient.upload(covertByteArrayToFlux(data),
getTransferOptions(blockSize), true)
.doOnSuccess(blockBlobItem -> logger.info("Successfully uploaded !!"))
.doOnError(throwable -> logger.error(
"Error occurred while uploading !! Exception:{}",
throwable.getMessage()))
.subscribe();
}
/**
* Covert byte array to flux flux.
*
* #param byteArray the byte array
* #return the flux
*/
public Flux<ByteBuffer> covertByteArrayToFlux(byte[] byteArray) {
return Flux.just(ByteBuffer.wrap(byteArray));
}
/**
* Creating TransferOptions.
*
* #param blockSize represents block size
* #return ParallelTransferOptions transfer options
*/
public ParallelTransferOptions getTransferOptions(long blockSize) {
return new ParallelTransferOptions()
.setBlockSizeLong(blockSize)
.setMaxConcurrency(5)
.setProgressReceiver(
bytesTransferred -> logger.info("Uploading bytes:{}", bytesTransferred));
}
For more details and code you can refer to my github repo
https://github.com/kdevesh/azure-storage-spring-boot-app
P.S. I am using the async flavour of Blob Client there is a sync flavour also available if somebody wants to use that.
Another way to get the Access token is
using the MS authentication lib
This library used "builders" to build out the confidential client. If you use that class, it handles refreshing the token for you and handles the cache
I use the method as Jack Jia , but it don't work.....i can got the token ,but when i upload , something wrong enter image description here
I have an Spring + CXF application which consumes a Transmission API: Transmission RPC running in another server.
According to Transmission docs, you need to send a token which is generated on the first request. The server then responds with a 409 http code along with a header containing the token. This token should be sent on all subsequent calls:
2.3.1. CSRF Protection Most Transmission RPC servers require a X-Transmission-Session-Id header to be sent with requests, to prevent
CSRF attacks. When your request has the wrong id -- such as when you
send your first request, or when the server expires the CSRF token --
the Transmission RPC server will return an HTTP 409 error with the
right X-Transmission-Session-Id in its own headers. So, the correct
way to handle a 409 response is to update your
X-Transmission-Session-Id and to resend the previous request.
I was looking for solution either using a CXF filter or interceptor, that basically will handle the 409 response and retry the initial request adding the token header. I'm thinking that clients can persist this token and send it in future calls.
I'm not very familiar with cxf so I was wondering if this can be accomplish and how. Any hint would be helpful.
Thanks!
Here spring-retry can be utilized which is now an independent project and no longer part of spring-batch.
As explained here retry callback will help make another call updated with the token header.
Pseudo code / logic in this case would look something like below
RetryTemplate template = new RetryTemplate();
Foo foo = template.execute(new RetryCallback<Foo>() {
public Foo doWithRetry(RetryContext context) {
/*
* 1. Check if RetryContext contains the token via hasAttribute. If available set the header else proceed
* 2. Call the transmission API
* 3.a. If API responds with 409, read the token
* 3.a.1. Store the token in RetryContext via setAttribute method
* 3.a.2. Throw a custom exception so that retry kicks in
* 3.b. If API response is non 409 handle according to business logic
* 4. Return result
*/
}
});
Make sure to configure the RetryTemplate with reasonable retry & backoff policies so as to avoid any resource contention / surprises.
Let know in comments in case of any queries / roadblock.
N.B.: RetryContext's implementation RetryContextSupport has the hasAttribute & setAttribute method inherited from Spring core AttributeAccessor
Assuming you are using Apache CXF JAX RS Client it is easy to do by just creating a custom Runtime Exception and ResponseExceptionMapper for it. So the idea is to manually convert 409 outcomes to some exception and then handle them correctly (in your case retry the service call).
See following code snipped for fully working example.
#SpringBootApplication
#EnableJaxRsProxyClient
public class SpringBootClientApplication {
// This can e stored somewhere in db or elsewhere
private static String lastToken = "";
public static void main(String[] args) {
SpringApplication.run(SpringBootClientApplication.class, args);
}
#Bean
CommandLineRunner initWebClientRunner(final TransmissionService service) {
return new CommandLineRunner() {
#Override
public void run(String... runArgs) throws Exception {
try {
System.out.println(service.sayHello(1, lastToken));
// catch the TokenExpiredException get the new token and retry
} catch (TokenExpiredException ex) {
lastToken = ex.getNewToken();
System.out.println(service.sayHello(1, lastToken));
}
}
};
}
public static class TokenExpiredException extends RuntimeException {
private String newToken;
public TokenExpiredException(String token) {
newToken = token;
}
public String getNewToken() {
return newToken;
}
}
/**
* This is where the magic is done !!!!
*/
#Provider
public static class TokenExpiredExceptionMapper implements ResponseExceptionMapper<TokenExpiredException> {
#Override
public TokenExpiredException fromResponse(Response r) {
if (r.getStatus() == 409) {
return new TokenExpiredException(r.getHeaderString("X-Transmission-Session-Id"));
}
return null;
}
}
#Path("/post")
public interface TransmissionService {
#GET
#Path("/{a}")
#Produces(MediaType.APPLICATION_JSON_VALUE)
String sayHello(#PathParam("a") Integer a, #HeaderParam("X-Transmission-Session-Id") String sessionId)
throws TokenExpiredException;
}
}
I'm trying to implement a login(remember me) application, and i'm having a problem to manage the cookies. when i'm doing Register(for new members) or login(for members that already have been registered) i'm doing in the server:
Cookie returnVisitorCookie = new Cookie("repeatVisitor", "yes");
returnVisitorCookie.setMaxAge(60*60*24*365); // 1 year
response.addCookie(returnVisitorCookie);
where the response i'm getting from the browser. for example : visitor.login(response).
When i'm doing SIGNOUT, i'm deleting the cookie. but it's seems that i have more cookies
that it should be, i mean if i registered 2 members and signout, i still have cookies with name = "repeatVisitor" and value = "yes".
Maybe because i'm putting the cookie in different respones.
Anybody can give me an idae what i'm doing wrong and how should i implement this?
Thank you
I sometimes find the best way to learn or understand is by looking at an example. Here is some code we use for a working website:
#WebServlet(name = "Login", urlPatterns = {"/authorization/Login"})
public class Login extends HttpServlet {
/**
* Processes requests for both HTTP
* <code>GET</code> and
* <code>POST</code> methods.
*
* #param request servlet request
* #param response servlet response
* #throws ServletException if a servlet-specific error occurs
* #throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
System.out.println("Reached login");
if (!Authorization.isLoggedIn(request)) {
String login = request.getParameter("login");
String password = request.getParameter("password");
boolean remember = Boolean.parseBoolean(request.getParameter("remember"));
System.out.println("Reached login "+login+", "+password+","+remember);
if (!Authorization.validateLogin(login, password)) {
Logger.getLogger(Login.class.getName()).log(Level.INFO,
"Failed login (invalid password) from {0} for {1}",
new String[]{request.getRemoteAddr(), login});
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid username or password!");
return;
}
//So far so good... Get the user object from the database (unique login names)
DB_User user = DB_User.get(login);
if (!user.getActive()) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Your account is no longer active!");
return;
}
String sessionID = Authorization.createNewSession(user, request.getRemoteAddr(), remember);
Cookie sessionCookie = new Cookie("my_application.session_id", sessionID);
sessionCookie.setDomain(request.getServerName());
sessionCookie.setPath(request.getContextPath());
if (remember) {
sessionCookie.setMaxAge(ServerConfig.getLoginSessionTimeout());
}
response.addCookie(sessionCookie);
}
response.sendRedirect("/app/myAccount.jsp");
} catch (Throwable ex) {
Logger.getLogger(Login.class.getName()).log(Level.SEVERE, null, ex);
ServletUtils.handleException(ex, response);
} finally {
out.flush();
out.close();
}
}
// +HttpSerlet default methods here. (doGet, doPost, getServletInfo)
}
Logout servlet example:
#WebServlet(name = "Logout", urlPatterns = {"/authorization/Logout"})
public class Logout extends HttpServlet {
/**
* Processes requests for both HTTP
* <code>GET</code> and
* <code>POST</code> methods.
*
* #param request servlet request
* #param response servlet response
* #throws ServletException if a servlet-specific error occurs
* #throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
String sessionID = ServletUtils.getCookieValue(request.getCookies(),"my_application.session_id");
if (sessionID != null) {
SQLManager sql = ServerConfig.getSql();
sql.deleteFromTable("login_session", "session_id = " + SQLString.toSql(sessionID));
Cookie sessionCookie = new Cookie("my_application.session_id", null);
sessionCookie.setDomain(ServletUtils.getCookieDomain(request));
sessionCookie.setPath("/you_app_name");
sessionCookie.setMaxAge(0);
response.addCookie(sessionCookie);
}
response.sendRedirect("/security/login.jsp");
} catch (Throwable ex) {
Logger.getLogger(Logout.class.getName()).log(Level.SEVERE, null, ex);
ServletUtils.handleException(ex, response);
} finally {
out.close();
}
}
}
There are some helper classes we have made as you will notice but the concept is there nonetheless. Hope this helps
You'll keep having the repeatVisitor cookie present in subsequent requests for at least a year, you've instructed the client's browser to do just that, "keep the cookie alive for a year". Removing the cookie from subsequent request headers won't stop a browser from simply re-adding it.
To implement remember me successfully, you need to
Use a secure token, not just a flag that says "repeat user : yes". Generate a unique token using something like java's UUID class to uniquely identify a visitor securely, so someone just doesn't intercept a request and put something in the header for you to deal with
Actively manage the secure token you generated per user. That means you store the token you generated in some persistent storage and check repeat requests against this store. It is within this store that you will manage token expiry and so forth. So in your persistent store, you can flag the token as inactive or expired
As an alternative, the easier remember me route is to set your remember-me to a fixed time duration for which you'll now set the cookie expiry date in the HttpServletRequest object. That is, you'll have your check box saying remember me for 2 weeks and then set your cookie expiry to 2 weeks. Subsequent representments of the same cookie token will be automatically managed without stress.
I want to implement basic authentication for Jetty server programmatically, as shown here. For the sake of convenience, I am ^C-^V'ing that snippet here.
import org.mortbay.jetty.security.*;
Server server = new Server();
Connector connector = new SelectChannelConnector();
connector.setPort(8080);
server.setConnectors(new Connector[]{connector});
Constraint constraint = new Constraint();
constraint.setName(Constraint.__BASIC_AUTH);;
constraint.setRoles(new String[]{"user","admin","moderator"});
constraint.setAuthenticate(true);
ConstraintMapping cm = new ConstraintMapping();
cm.setConstraint(constraint);
cm.setPathSpec("/*");
SecurityHandler sh = new SecurityHandler();
sh.setUserRealm(new HashUserRealm("MyRealm",System.getProperty("jetty.home")+"/etc/realm.properties"));
sh.setConstraintMappings(new ConstraintMapping[]{cm});
WebAppContext webappcontext = new WebAppContext();
webappcontext.setContextPath("/mywebapp");
webappcontext.setWar("./path/to/my/war/orExplodedwar");
webappcontext.addHandler(sh);
HandlerCollection handlers= new HandlerCollection();
handlers.setHandlers(new Handler[]{webappcontext, new DefaultHandler()});
server.setHandler(handlers);
server.start();
server.join();
Now the problem is that the above approach requires you to have a handle to the server. However in my case, since I am using Camel, I do not have a direct access to the server. This is how my pipeline is defined:
from("jetty:http://localhost:8080/documents_in?matchOnUriPrefix=true").
process(new MyProcessor());
How do I adapt the linked authentication solution to my case? Or do I have to follow some completely different method?
Please note that I am both a Camel and Jetty novice. Any help will be greatly appreciated. Thanks.
Addendum:
This page shows how to do it with Spring XML, however we are not using Spring, so that's of no use to us.
I stumbled across this problem a couple of days ago and I solved this issue by defining an own implementation of ConstraintSecurityHandler which uses a customized LoginService which takes care of the authentication BasicAuthenticator requires. As I did not find any existing LoginService implementation that is capable of dealing with bean-managed authentication, I needed to come up with this solution.
I'll post the almost complete class except for the internal stuff which has to be kept private.
import java.security.Principal;
import javax.annotation.Resource;
import javax.security.auth.Subject;
import org.eclipse.jetty.security.ConstraintMapping;
import org.eclipse.jetty.security.ConstraintSecurityHandler;
import org.eclipse.jetty.security.DefaultIdentityService;
import org.eclipse.jetty.security.IdentityService;
import org.eclipse.jetty.security.LoginService;
import org.eclipse.jetty.security.MappedLoginService;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.util.security.Credential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Strings;
/**
* <p>
* Sets up a basic authentication mechanism for REST based services exposed via
* Jetty for our REST API (http(s)://server:port/api/v1/...).
* </p>
* <p>
* It moreover defines a login service which is capable of using an internal
* persistence layer for authenticating a user and his credentials received via
* a challenge response against a user entity retrieved via the persistence
* layer.
* </p>
*/
public class JettyBasicAuthAuthorizationHandler extends ConstraintSecurityHandler
{
/** The logger of this class **/
private static final Logger logger =
LoggerFactory.getLogger(JettyBasicAuthAuthorizationHandler.class);
/** The persistence service to retrieve the user informations from **/
#Resource
private ISomePersistenceService persistenceService;
private final String[] roles = new String[] {"user"};
/**
* <p>
* Initializes a Jetty based Basic Authentication mechanism.
* </p>
*/
public JettyBasicAuthAuthorizationHandler()
{
super();
// Specifies the challenge to be of type BASIC and that users have
// to fulfill one of the roles listed in roles. Moreover authentication
// is required
Constraint constraint = new Constraint();
constraint.setName(Constraint.__BASIC_AUTH);
constraint.setRoles(this.roles);
constraint.setAuthenticate(true);
// Map the defined constraints from above to the services provided via
// our REST API
ConstraintMapping cm = new ConstraintMapping();
cm.setConstraint(constraint);
cm.setPathSpec("/api/v1/*");
// BasicAuthenticator takes care of sending a challenge to the caller
// and calls our login service in case of a challenge response to
// evaluate if the user is permitted to use the service.
// The realm name defines the name of the login service which should be
// used for authentication.
BasicAuthenticator basic = new BasicAuthenticator();
this.setAuthenticator(basic);
this.addConstraintMapping(cm);
this.setRealmName("REST");
this.setLoginService(new BeanManagedLoginService("REST"));
logger.debug("JettyBasicAuthAuthorizationHandler created!");
}
/**
* <p>
* Implements a bean managed login service where an authentication response
* is propagated to a business layer bean which retrieves the user and
* credentials from a backing data store.
* </p>
*/
class BeanManagedLoginService implements LoginService
{
/** An identity service used to create a UserIdentity object for us **/
private IdentityService identityService = new DefaultIdentityService();
private String name = "REST";
/**
* <p>
* Initializes a new instance.
* </p>
*/
public BeanManagedLoginService()
{
}
/**
* <p>
* Initializes a new instance and sets the realm name this login service
* will work for.
* </p>
*
* #param name The name of this login service (also known as the realm it
* will work for)
*/
public BeanManagedLoginService(String name)
{
this.name = name;
}
/**
* <p>
* Returns the name of the login service (the realm name)
* </p>
*
* #return Get the name of the login service (aka Realm name)
*/
#Override
public String getName()
{
return this.name;
}
/**
* <p>
* Logs in a user by checking the username with known users and
* comparing the credentials with the stored ones. If the user could not
* be authenticated successfully an unauthenticated user identity will
* be returned.
* </p>
*
* #param username The user name as sent by the ChallengeResponse
* #param credentials The credentials provided in the ChallengeResponse
*
* #return If the user could be authenticated successfully a valid
* {#link UserIdentity}, else an unauthorized user identity
*/
#Override
public UserIdentity login(String username, Object credentials)
{
if (logger.isDebugEnabled())
logger.debug("received login request for user: '{}' with credentials: '{}'!",
username, credentials);
// check if the username is valid
if (!Strings.isNullOrEmpty(username))
{
String password = credentials.toString();
// retrieve the user from the business layer
final UserEntity sue = persistenceService.findUser(username);
if (sue == null)
{
if (logger.isErrorEnabled())
logger.error("No User could be found for UserId '{}'. The UserKey (which was not checked) is '{}'",
username, password);
return UserIdentity.UNAUTHENTICATED_IDENTITY;
}
// check whether the password matches the one in the user entity
// found for the user id
if (password.equals(sue.getUserKey()))
{
// the user could be successfully authenticated
if (logger.isDebugEnabled())
logger.debug("UserKey {} of User {} was successfully authenticated",
sue.getUserKey(), sue.getUserId());
if (logger.isDebugEnabled())
logger.debug("User '{}'/'{}' works for '{}'",
userId, userName, sue.getCompany().getName());
return this.createIdentityForUser(username, password);
}
else
{
// the password set in the request and the one stored in the
// user entity do not match
if (logger.isErrorEnabled())
logger.error(
"User {} could not be authenticated. The UserKey in the user entity is {} but the UserKey in the request was {}",
new Object[] { username, sue.getUserKey(), password });
return UserIdentity.UNAUTHENTICATED_IDENTITY;
}
}
else
{
if (logger.isErrorEnabled())
logger.error("Username is empty and therefore could not get authenticated correctly");
return UserIdentity.UNAUTHENTICATED_IDENTITY;
}
}
/**
* <p>
* Creates a UserIdentity object for a successfully authenticated user.
* </p>
*
* #param username The name of the authenticated user
* #param password The password of the authenticated user
*
* #return A valid UserIdentity object
*/
private UserIdentity createIdentityForUser(String username, String password)
{
// create a principal object needed for the user identity
Credential cred = Credential.getCredential(password);
// a principal is basically an identity of a real person
// (subject). So a user can have multiple principals
Principal userPrincipal = new MappedLoginService.KnownUser(username, cred);
// a subject collects all data necessary to identify a certain
// person. It may store multiple identities and passwords or
// cryptographic keys
Subject subject = new Subject();
// add a Principal and credential to the Subject
subject.getPrincipals().add(userPrincipal);
subject.getPrivateCredentials().add(cred);
subject.setReadOnly();
return this.identityService.newUserIdentity(subject, userPrincipal, roles);
}
/**
* <p>
* Validate just checks if a user identity is still valid.
* </p>
*/
#Override
public boolean validate(UserIdentity user)
{
return true;
}
#Override
public IdentityService getIdentityService()
{
return this.identityService;
}
#Override
public void setIdentityService(IdentityService service)
{
this.identityService = service;
}
#Override
public void logout(UserIdentity user)
{
}
}
}
To add this handler to Camel's embedded Jetty server you can define an endpoint using this handler like this:
jetty:https://your-server:port/api/v1/yourService?sslContextParameters=#sslContextParameters&handlers=#jettyAuthHandler
where jettyAuthHandler is the bean name of this handler - if you don't use SSL just omit the sslContextParameters parameter.
The JettyComponent in Camel has getter/setter where you can configure this in Java code.
JettyComponent jetty = new JettyComponent();
// use getter/setter to configure
// add component to Camel
camelContext.addComponent("jetty", jetty);
// after this you can add the routes and whatnot
I'm implementing a JAX-WS webservice that will be consumed by external Java and PHP clients.
The clients have to authenticate with a username and password stored in a database per client.
What authentication mechanism is best to use to make sure that misc clients can use it?
For our Web Service authentication we are pursuing a twofold approach, in order to make sure that clients with different prerequisites are able to authenticate.
Authenticate using a username and password parameter in the HTTP Request Header
Authenticate using HTTP Basic Authentication.
Please note, that all traffic to our Web Service is routed over an SSL secured connection. Thus, sniffing the passwords is not possible. Of course one may also choose HTTP authentication with digest - see this interesting site for more information on this.
But back to our example:
//First, try authenticating against two predefined parameters in the HTTP
//Request Header: 'Username' and 'Password'.
public static String authenticate(MessageContext mctx) {
String s = "Login failed. Please provide a valid 'Username' and 'Password' in the HTTP header.";
// Get username and password from the HTTP Header
Map httpHeaders = (Map) mctx.get(MessageContext.HTTP_REQUEST_HEADERS);
String username = null;
String password = null;
List userList = (List) httpHeaders.get("Username");
List passList = (List) httpHeaders.get("Password");
// first try our username/password header authentication
if (CollectionUtils.isNotEmpty(userList)
&& CollectionUtils.isNotEmpty(passList)) {
username = userList.get(0).toString();
password = passList.get(0).toString();
}
// No username found - try HTTP basic authentication
if (username == null) {
List auth = (List) httpHeaders.get("Authorization");
if (CollectionUtils.isNotEmpty(auth)) {
String[] authArray = authorizeBasic(auth.get(0).toString());
if (authArray != null) {
username = authArray[0];
password = authArray[1];
}
}
}
if (username != null && password != null) {
try {
// Perform the authentication - e.g. against credentials from a DB, Realm or other
return authenticate(username, password);
} catch (Exception e) {
LOG.error(e);
return s;
}
}
return s;
}
/**
* return username and password for basic authentication
*
* #param authorizeString
* #return
*/
public static String[] authorizeBasic(String authorizeString) {
if (authorizeString != null) {
StringTokenizer st = new StringTokenizer(authorizeString);
if (st.hasMoreTokens()) {
String basic = st.nextToken();
if (basic.equalsIgnoreCase("Basic")) {
String credentials = st.nextToken();
String userPass = new String(
Base64.decodeBase64(credentials.getBytes()));
String[] userPassArray = userPass.split(":");
if (userPassArray != null && userPassArray.length == 2) {
String userId = userPassArray[0];
String userPassword = userPassArray[1];
return new String[] { userId, userPassword };
}
}
}
}
return null;
}
The first authentication using our predefined "Username" and "Password" parameters is in particular useful for our integration testers, who are using SOAP-UI (Although I am not entirely sure whether one cannot get to work SOAP-UI with HTTP Basic Authentication too). The second authentication attempt then uses the parameters which are provided by HTTP Basic authentication.
In order to intercept every call to the Web Service, we define a handler on every endpoint:
#HandlerChain(file = "../../../../../handlers.xml")
#SchemaValidation(handler = SchemaValidationErrorHandler.class)
public class DeliveryEndpointImpl implements DeliveryEndpoint {
The handler.xml looks like:
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee">
<handler-chain>
<handler>
<handler-name>AuthenticationHandler</handler-name>
<handler-class>mywebservice.handler.AuthenticationHandler</handler-class>
</handler>
</handler-chain>
</handler-chains>
As you can see, the handler points to an AuthenticationHandler, which intercepts every call to the Web Service endpoint. Here's the Authentication Handler:
public class AuthenticationHandler implements SOAPHandler<SOAPMessageContext> {
/**
* Logger
*/
public static final Log log = LogFactory
.getLog(AuthenticationHandler.class);
/**
* The method is used to handle all incoming messages and to authenticate
* the user
*
* #param context
* The message context which is used to retrieve the username and
* the password
* #return True if the method was successfully handled and if the request
* may be forwarded to the respective handling methods. False if the
* request may not be further processed.
*/
#Override
public boolean handleMessage(SOAPMessageContext context) {
// Only inbound messages must be authenticated
boolean isOutbound = (Boolean) context
.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (!isOutbound) {
// Authenticate the call
String s = EbsUtils.authenticate(context);
if (s != null) {
log.info("Call to Web Service operation failed due to wrong user credentials. Error details: "
+ s);
// Return a fault with an access denied error code (101)
generateSOAPErrMessage(
context.getMessage(),
ServiceErrorCodes.ACCESS_DENIED,
ServiceErrorCodes
.getErrorCodeDescription(ServiceErrorCodes.ACCESS_DENIED),
s);
return false;
}
}
return true;
}
/**
* Generate a SOAP error message
*
* #param msg
* The SOAP message
* #param code
* The error code
* #param reason
* The reason for the error
*/
private void generateSOAPErrMessage(SOAPMessage msg, String code,
String reason, String detail) {
try {
SOAPBody soapBody = msg.getSOAPPart().getEnvelope().getBody();
SOAPFault soapFault = soapBody.addFault();
soapFault.setFaultCode(code);
soapFault.setFaultString(reason);
// Manually crate a failure element in order to guarentee that this
// authentication handler returns the same type of soap fault as the
// rest
// of the application
QName failureElement = new QName(
"http://yournamespacehere.com", "Failure", "ns3");
QName codeElement = new QName("Code");
QName reasonElement = new QName("Reason");
QName detailElement = new QName("Detail");
soapFault.addDetail().addDetailEntry(failureElement)
.addChildElement(codeElement).addTextNode(code)
.getParentElement().addChildElement(reasonElement)
.addTextNode(reason).getParentElement()
.addChildElement(detailElement).addTextNode(detail);
throw new SOAPFaultException(soapFault);
} catch (SOAPException e) {
}
}
/**
* Handles faults
*/
#Override
public boolean handleFault(SOAPMessageContext context) {
// do nothing
return false;
}
/**
* Close - not used
*/
#Override
public void close(MessageContext context) {
// do nothing
}
/**
* Get headers - not used
*/
#Override
public Set<QName> getHeaders() {
return null;
}
}
In the AuthenticationHandler we are calling the authenticate() method, defined further above. Note that we create a manual SOAP fault called "Failure" in case something goes wrong with the authentication.
Basic WS-Security would work with both Java and PHP clients (amongst others) plugged in to JAAS to provide a database backend . How to implement that kind of depends on your container. Annotate your web service methods with the #RolesAllowed annotation to control which roles the calling user must have. All J2EE containers will provide some mechanism to specify against which JAAS realm users should be authenticated. In Glassfish for example, you can use the admin console to manage realms, users and groups. In your application.xml you then specify the realm and the group to role mappings.
Here are some details of how to achieve this on Glassfish
With JBoss WS in JBoss, it's even easier.
What JAX-WS implementation are you using and in which container?
Is there a way independent on the current container? I'd like to define which class is responsible for authorisation. That class could call database or have password elsewhere.