Skip to content
This repository has been archived by the owner on Apr 21, 2020. It is now read-only.

Commit

Permalink
Add a JWT test client in Java.
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanbreen committed Sep 27, 2015
1 parent e151b4a commit 88a0bb0
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 23 deletions.
2 changes: 1 addition & 1 deletion lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ var bunyan = require('bunyan');
var default_config = {
name: 'oauth_reverse_proxy',
streams: [{
level: process.env.OAUTH_REVERSE_PROXY_LOG_LEVEL || "warn",
level: process.env.OAUTH_REVERSE_PROXY_LOG_LEVEL || "trace",
stream: process.stdout
}]
};
Expand Down
18 changes: 9 additions & 9 deletions lib/proxy/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,21 @@ function getMiddlewareList(this_obj) {
oauth1a: true,
jwt: false,
value: require('./validators/oauth_param_sanity_validator.js')(this_obj)
}, {
name: 'jwt_validator',
desc: 'Perform the JWT validation.',
forward_proxy: false,
reverse_proxy: true,
oauth1a: false,
jwt: true,
value: require('./validators/json_web_token_validator.js')(this_obj)
}, {
name: 'quota_validator',
desc: 'Validate that the request is within quota.',
forward_proxy: true,
reverse_proxy: true,
oauth1a: true,
jwt: true,
jwt: false,
value: require('./validators/quota_validator.js')(this_obj)
}, {
name: 'oauth_timestamp_validator',
Expand All @@ -141,14 +149,6 @@ function getMiddlewareList(this_obj) {
oauth1a: true,
jwt: false,
value: require('./validators/oauth_signature_validator.js')(this_obj)
}, {
name: 'jwt_validator',
desc: 'Perform the JWT validation.',
forward_proxy: false,
reverse_proxy: true,
oauth1a: false,
jwt: true,
value: require('./validators/json_web_token_validator.js')(this_obj)
}, {
name: 'oauth_param_generator',
desc: 'Validate that the timestamp of the request is legal',
Expand Down
2 changes: 2 additions & 0 deletions lib/proxy/jwt/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

exports.ISSUER_HEADER = 'x-oauth-reverse-proxy-jwt-issuer';
11 changes: 9 additions & 2 deletions lib/proxy/validators/json_web_token_validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ var jwt = require('express-jwt');

var unauthorized = require('../messages/unauthorized.js');

var ISSUER_HEADER = require('../jwt/constants.js').ISSUER_HEADER;

var module_tag = {
module: require('../../logger.js').getModulePath(__filename)
};
Expand All @@ -14,20 +16,25 @@ module.exports = function(proxy) {
var keys = proxy.keystore.keys;

return function(req, res, next) {
var issuer = undefined;
var jwt_validator = jwt({
secret: function(req, payload, done) {
// TODO: We need to validate that this was set already
var issuer = payload.iss;
issuer = payload.iss;
if (issuer === undefined) return unauthorized(proxy.logger, req, res, "No issuer specified");
if (keys[issuer] === undefined) return unauthorized(proxy.logger, req, res, "Invalid issuer specified");
done(null, keys[issuer]);

proxy.logger.info("issuer is '%s'", issuer);

done(null, keys[issuer].secret);
}
});

jwt_validator(req, res, function(err) {
if (err) return unauthorized(proxy.logger, req, res, "JSON web token validation error " + err);

// TODO: Grab req.user.admin and create a header out of it?
req.headers[ISSUER_HEADER] = issuer;

// We passed validation, so move to the next step in the pipeline.
next();
Expand Down
14 changes: 10 additions & 4 deletions test/client_library_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ describe('An OAuth-compliant reverse proxy', function() {

// TODO: Refactor these tests so they auto-detect if they can be run on the host system. This would simplify
// the platform-specific logic in here and allow for successful tests with less lengthy setup on CI.

/**
it ('should support requests from Ruby clients', function(done) {
var rubyTest = create_client_test('GET', 'ruby client.rb', 'test/clients/ruby', 'ruby-test-key')
rubyTest(done);
});

**/
it ('should support requests from Java clients', function(done) {
var javaTest = create_client_test('POST',
'java -cp target/JWTClient-1.0-SNAPSHOT-jar-with-dependencies.jar com.cimpress.mcp.jwt.JWTClient',
'test/clients/java/JWTClient', 'java-test-key')
javaTest(done);
});
/**
var javaTest = create_client_test('POST',
'java -cp target/OAuthClient-1.0-SNAPSHOT-jar-with-dependencies.jar com.cimpress.mcp.oauth.OAuthClient',
'test/clients/java/OAuthClient', 'java-test-key')
Expand All @@ -56,7 +62,7 @@ describe('An OAuth-compliant reverse proxy', function() {
var perlTest = create_client_test('GET', 'perl client.pl', 'test/clients/perl', 'perl-test-key')
perlTest(done);
});
**/
**
// Mac-specific client tests
if(os.platform() === "darwin") {
Expand Down Expand Up @@ -125,6 +131,6 @@ describe('An OAuth-compliant reverse proxy', function() {
var pythonTest = create_client_test('GET', 'python client.py', 'test/clients/python', 'python-test-key')
pythonTest(done);
});
}
}**/

});
60 changes: 60 additions & 0 deletions test/clients/java/JWTClient/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.cimpress.mcp.jwt</groupId>
<artifactId>JWTClient</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>JWT Client Test</name>
<url>http://maven.apache.org</url>

<dependencies>
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.4.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>

</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.cimpress.mcp.jwt;

import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.jose4j.base64url.Base64;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;

public class JWTClient {
private static final String CONSUMER_KEY = "java-test-key";

public static void main(String[] args) throws Exception {

File keyFile = new File("../../../keys/8008/8080/" + CONSUMER_KEY);
String secret = Base64.encode(IOUtils.toString(new FileInputStream(keyFile)).getBytes());

URL url = new URL("http://localhost:7070/job?this=is&fun=right");

HttpPost request = new HttpPost(url.toURI());
List<NameValuePair> params = new LinkedList<NameValuePair>();
// NOTE: The below line doesn't work because Java SignPost can't handle query and post
// params with the same name.
// params.add(new BasicNameValuePair("this", "post"));
params.add(new BasicNameValuePair("post", "happy"));
params.add(new BasicNameValuePair("wow", "so"));
params.add(new BasicNameValuePair("signposty", "a"));
params.add(new BasicNameValuePair("signposty", "b"));
params.add(new BasicNameValuePair("signposty", "rad"));
//request.setEntity(new UrlEncodedFormEntity(params));

// sign the request
JwtClaims claims = new JwtClaims();
claims.setIssuer("java-test-key");
claims.setGeneratedJwtId();

// A JWT is a JWS and/or a JWE with JSON claims as the payload.
// In this example it is a JWS so we create a JsonWebSignature object.
JsonWebSignature jws = new JsonWebSignature();

// The payload of the JWS is JSON content of the JWT Claims
jws.setPayload(claims.toJson());

// The JWT is signed using the private key
String jwkJson = "{\"kty\":\"oct\",\"k\":\""+ secret +"\"}";
JsonWebKey key = JsonWebKey.Factory.newJwk(jwkJson);
jws.setKey(key.getKey());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);

String jwt = jws.getCompactSerialization();

request.setHeader("Authorization", "Bearer " + jwt);

// send the request
HttpClient httpClient = HttpClientBuilder.create().build();
HttpResponse response = httpClient.execute(request);

// Print the result
System.out.println(IOUtils.toString(response.getEntity().getContent()));
}
}
20 changes: 13 additions & 7 deletions test/job_server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ app.PUT = app.put;
app.DELETE = app.delete;

const CONSUMER_KEY_HEADER = require('../../lib/proxy/oauth/constants.js').CONSUMER_KEY_HEADER;
const ISSUER_HEADER = require('../../lib/proxy/jwt/constants.js').ISSUER_HEADER;

function getUserHeader(req) {
if (req.headers[CONSUMER_KEY_HEADER]) return req.headers[CONSUMER_KEY_HEADER];
if (req.headers[ISSUER_HEADER]) return req.headers[ISSUER_HEADER];
}

// The job server represents they types of servers we might expect to see behind oauth_reverse_proxy.
function JobServer() {
Expand All @@ -26,7 +32,7 @@ function JobServer() {
['GET', 'DELETE'].forEach(function(verb) {
app[verb]("/job/:job_id", function(req, res) {
res.setHeader('Content-Type', 'application/json');
console.log('%s with key %s', verb, req.headers[CONSUMER_KEY_HEADER]);
console.log('%s with key %s', verb, getUserHeader(req));
this_obj.emit(verb + " /job", req, res);
res.send({'status':'ok'});
});
Expand All @@ -50,7 +56,7 @@ function JobServer() {
['GET', 'POST', 'PUT', 'DELETE'].forEach(function(verb) {
app[verb]('/%7bwonky%20path%7d/is&wonky', function(req, res, next) {
res.setHeader('Content-Type', 'application/json');
console.log('%s /{wonky path}/is&wonky with key %s', verb.toUpperCase(), req.headers[CONSUMER_KEY_HEADER]);
console.log('%s /{wonky path}/is&wonky with key %s', verb.toUpperCase(), getUserHeader(req));
this_obj.emit(verb + ' /{wonky path}/is&wonky', req, res);
res.send({'status':'ok'});
});
Expand All @@ -61,7 +67,7 @@ function JobServer() {
app[verb]('/compressed_content', function(req, res, next) {

res.setHeader('Content-Type', 'text/plain');
console.log('%s /compressed_content with key %s', verb.toUpperCase(), req.headers[CONSUMER_KEY_HEADER]);
console.log('%s /compressed_content with key %s', verb.toUpperCase(), getUserHeader(req));

compress()(req, res, function() {
res.write(fs.readFileSync('./test/resources/lorem_ipsum.txt'), 'utf8');
Expand All @@ -75,7 +81,7 @@ function JobServer() {
app[verb]('/transactions', function(req, res) {

res.setHeader('Content-Type', 'application/json');
console.log('%s /transactions with key %s', verb.toUpperCase(), req.headers[CONSUMER_KEY_HEADER]);
console.log('%s /transactions with key %s', verb.toUpperCase(), getUserHeader(req));

// Generate a sizeable chunk of json that will be chunked and returned
res.write('{\n\t"jobs_list":[');
Expand All @@ -96,7 +102,7 @@ function JobServer() {
});

app.POST('/getProducts', function(req, res) {
console.log('POST /getProducts with key %s', req.headers[CONSUMER_KEY_HEADER]);
console.log('POST /getProducts with key %s', getUserHeader(req));

var data = '';

Expand All @@ -117,14 +123,14 @@ function JobServer() {

app[verb]("/job", function(req, res) {
res.setHeader('Content-Type', 'application/json');
console.log('%s with key %s', verb, req.headers[CONSUMER_KEY_HEADER]);
console.log('%s with key %s', verb, getUserHeader(req));
this_obj.emit(verb + " /job", req, res);
res.send({'status':'ok'});
});

// /uploads simulates an endpoint that receives multipart form data for posts and puts
app[verb]('/uploads', function(req, res) {
console.log('%s /uploads with key %s', verb.toUpperCase(), req.headers[CONSUMER_KEY_HEADER]);
console.log('%s /uploads with key %s', verb.toUpperCase(), getUserHeader(req));

var file_path = './test/resources/' + req.files['binary_data'][0].originalname;
var expected_length = req.files['binary_data'][0].size;
Expand Down

0 comments on commit 88a0bb0

Please sign in to comment.