Skip to content

Commit ad54637

Browse files
committed
Session: JWT Web Token implementation
1 parent 866ff39 commit ad54637

9 files changed

Lines changed: 446 additions & 19 deletions

File tree

jooby/src/main/java/io/jooby/Cookie.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
package io.jooby;
77

8+
import com.typesafe.config.Config;
9+
810
import javax.annotation.Nonnull;
911
import javax.annotation.Nullable;
1012
import javax.crypto.Mac;
@@ -22,7 +24,10 @@
2224
import java.util.HashMap;
2325
import java.util.Locale;
2426
import java.util.Map;
27+
import java.util.Optional;
2528
import java.util.concurrent.TimeUnit;
29+
import java.util.function.BiFunction;
30+
import java.util.function.Consumer;
2631

2732
/**
2833
* Response cookie implementation. Response are send it back to client using
@@ -458,12 +463,46 @@ public long getMaxAge() {
458463
start = end + 1;
459464
} while (start < len);
460465

461-
return attributes.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(attributes);
466+
return attributes.isEmpty()
467+
? Collections.emptyMap()
468+
: Collections.unmodifiableMap(attributes);
462469
} catch (UnsupportedEncodingException x) {
463470
throw SneakyThrows.propagate(x);
464471
}
465472
}
466473

474+
/**
475+
* Attempt to create/parse a cookie from application configuration object. The namespace given
476+
* must be present and must defined a <code>name</code> property.
477+
*
478+
* The namespace might optionally defined: value, path, domain, secure, httpOnly and maxAge.
479+
*
480+
* @param namespace Cookie namespace/prefix.
481+
* @param conf Configuration object.
482+
* @return Parsed cookie or empty.
483+
*/
484+
public static @Nonnull Optional<Cookie> create(@Nonnull String namespace, @Nonnull Config conf) {
485+
if (conf.hasPath(namespace)) {
486+
Cookie cookie = new Cookie(conf.getString(namespace + ".name"));
487+
value(conf, namespace + ".value", Config::getString, cookie::setValue);
488+
value(conf, namespace + ".path", Config::getString, cookie::setPath);
489+
value(conf, namespace + ".domain", Config::getString, cookie::setDomain);
490+
value(conf, namespace + ".secure", Config::getBoolean, cookie::setSecure);
491+
value(conf, namespace + ".httpOnly", Config::getBoolean, cookie::setHttpOnly);
492+
value(conf, namespace + ".maxAge", (c, path) -> c.getDuration(path, TimeUnit.SECONDS),
493+
cookie::setMaxAge);
494+
return Optional.of(cookie);
495+
}
496+
return Optional.empty();
497+
}
498+
499+
private static <T> void value(Config conf, String name, BiFunction<Config, String, T> mapper,
500+
Consumer<T> consumer) {
501+
if (conf.hasPath(name)) {
502+
consumer.accept(mapper.apply(conf, name));
503+
}
504+
}
505+
467506
private void append(StringBuilder sb, String str) {
468507
if (needQuote(str)) {
469508
sb.append('"');

jooby/src/main/java/io/jooby/SessionStore.java

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import javax.annotation.Nonnull;
1212
import javax.annotation.Nullable;
1313
import java.time.Instant;
14+
import java.util.Map;
15+
import java.util.function.Function;
1416

1517
/**
1618
* Load and save sessions from store (memory, database, etc.).
@@ -112,23 +114,47 @@ public interface SessionStore {
112114
* <code>HMAC_SHA256</code>. See {@link Cookie#sign(String, String)}.
113115
*
114116
* @param secret Secret token to signed data.
115-
* @param cookie Cookie to use.
116117
* @return A browser session store.
117118
*/
118-
static @Nonnull SessionStore cookie(@Nonnull String secret, @Nonnull Cookie cookie) {
119-
return new CookieSessionStore(secret, cookie);
119+
static @Nonnull SessionStore cookie(@Nonnull String secret) {
120+
return cookie(secret, SessionToken.SID);
120121
}
121122

122123
/**
123124
* Creates a session store that save data into Cookie. Cookie data is signed it using
124125
* <code>HMAC_SHA256</code>. See {@link Cookie#sign(String, String)}.
125126
*
126-
* It uses the default session cookie: {@link SessionToken#SID}.
127-
*
128127
* @param secret Secret token to signed data.
128+
* @param cookie Cookie to use.
129129
* @return A browser session store.
130130
*/
131-
static @Nonnull SessionStore cookie(@Nonnull String secret) {
132-
return cookie(secret, SessionToken.SID);
131+
static @Nonnull SessionStore cookie(@Nonnull String secret, @Nonnull Cookie cookie) {
132+
SneakyThrows.Function<String, Map<String, String>> decoder = value -> {
133+
String unsign = Cookie.unsign(value, secret);
134+
if (unsign == null) {
135+
return null;
136+
}
137+
return Cookie.decode(unsign);
138+
};
139+
140+
SneakyThrows.Function<Map<String, String>, String> encoder = attributes ->
141+
Cookie.sign(Cookie.encode(attributes), secret);
142+
143+
return new CookieSessionStore(cookie, decoder, encoder);
144+
}
145+
146+
/**
147+
* Creates a session store that save data into Cookie. Cookie data is (un)signed it using the given
148+
* decoder and encoder.
149+
*
150+
* @param cookie Cookie to use.
151+
* @param decoder Decoder to use.
152+
* @param encoder Encoder to use.
153+
* @return Cookie session store.
154+
*/
155+
static @Nonnull SessionStore cookie(@Nonnull Cookie cookie,
156+
@Nonnull Function<String, Map<String, String>> decoder,
157+
@Nonnull Function<Map<String, String>, String> encoder) {
158+
return new CookieSessionStore(cookie, decoder, encoder);
133159
}
134160
}

jooby/src/main/java/io/jooby/internal/CookieSessionStore.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
import io.jooby.Session;
1111
import io.jooby.SessionStore;
1212
import io.jooby.SessionToken;
13+
import io.jooby.SneakyThrows;
1314

1415
import javax.annotation.Nonnull;
1516
import javax.annotation.Nullable;
1617
import java.util.HashMap;
1718
import java.util.Map;
19+
import java.util.function.Function;
1820

1921
public class CookieSessionStore implements SessionStore {
2022

@@ -41,11 +43,16 @@ public CookieToken(@Nonnull Cookie cookie) {
4143

4244
private static final String NO_ID = "<missing>";
4345

44-
private final String secret;
46+
private final Function<String, Map<String, String>> decoder;
47+
48+
private final Function<Map<String, String>, String> encoder;
49+
4550
private final SessionToken token;
4651

47-
public CookieSessionStore(String secret, Cookie cookie) {
48-
this.secret = secret;
52+
public CookieSessionStore(Cookie cookie, Function<String, Map<String, String>> decoder,
53+
Function<Map<String, String>, String> encoder) {
54+
this.decoder = decoder;
55+
this.encoder = encoder;
4956
this.token = new CookieToken(cookie);
5057
}
5158

@@ -58,12 +65,8 @@ public CookieSessionStore(String secret, Cookie cookie) {
5865
if (signed == null) {
5966
return null;
6067
}
61-
String unsign = Cookie.unsign(signed, secret);
62-
if (unsign == null) {
63-
return null;
64-
}
65-
Map<String, String> attributes = Cookie.decode(unsign);
66-
if (attributes.isEmpty()) {
68+
Map<String, String> attributes = decoder.apply(signed);
69+
if (attributes == null || attributes.size() == 0) {
6770
return null;
6871
}
6972
return Session.create(ctx, NO_ID, new HashMap<>(attributes)).setNew(false);
@@ -74,8 +77,7 @@ public CookieSessionStore(String secret, Cookie cookie) {
7477
}
7578

7679
@Override public void touchSession(@Nonnull Context ctx, @Nonnull Session session) {
77-
String value = Cookie.encode(session.toMap());
78-
token.saveToken(ctx, Cookie.sign(value, secret));
80+
token.saveToken(ctx, encoder.apply(session.toMap()));
7981
}
8082

8183
@Override public void saveSession(@Nonnull Context ctx, @Nonnull Session session) {

modules/jooby-jwt/pom.xml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
6+
<parent>
7+
<groupId>io.jooby</groupId>
8+
<artifactId>modules</artifactId>
9+
<version>2.1.1-SNAPSHOT</version>
10+
</parent>
11+
12+
<modelVersion>4.0.0</modelVersion>
13+
<artifactId>jooby-jwt</artifactId>
14+
15+
<dependencies>
16+
<dependency>
17+
<groupId>com.google.code.findbugs</groupId>
18+
<artifactId>jsr305</artifactId>
19+
<scope>provided</scope>
20+
</dependency>
21+
22+
<dependency>
23+
<groupId>io.jooby</groupId>
24+
<artifactId>jooby</artifactId>
25+
<version>${jooby.version}</version>
26+
</dependency>
27+
28+
<dependency>
29+
<groupId>io.jsonwebtoken</groupId>
30+
<artifactId>jjwt-impl</artifactId>
31+
</dependency>
32+
33+
<dependency>
34+
<groupId>io.jsonwebtoken</groupId>
35+
<artifactId>jjwt-orgjson</artifactId>
36+
</dependency>
37+
38+
<!-- Test dependencies -->
39+
<dependency>
40+
<groupId>org.junit.jupiter</groupId>
41+
<artifactId>junit-jupiter-engine</artifactId>
42+
<scope>test</scope>
43+
</dependency>
44+
45+
<dependency>
46+
<groupId>org.jacoco</groupId>
47+
<artifactId>org.jacoco.agent</artifactId>
48+
<classifier>runtime</classifier>
49+
<scope>test</scope>
50+
</dependency>
51+
52+
<dependency>
53+
<groupId>org.mockito</groupId>
54+
<artifactId>mockito-core</artifactId>
55+
<scope>test</scope>
56+
</dependency>
57+
</dependencies>
58+
</project>
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package io.jooby.jwt;
2+
3+
import com.typesafe.config.Config;
4+
import io.jooby.Cookie;
5+
import io.jooby.Extension;
6+
import io.jooby.Jooby;
7+
import io.jooby.SessionStore;
8+
import io.jooby.SessionToken;
9+
import io.jooby.SneakyThrows;
10+
import io.jsonwebtoken.Claims;
11+
import io.jsonwebtoken.Jws;
12+
import io.jsonwebtoken.JwtBuilder;
13+
import io.jsonwebtoken.JwtException;
14+
import io.jsonwebtoken.Jwts;
15+
import io.jsonwebtoken.security.Keys;
16+
17+
import javax.annotation.Nonnull;
18+
import java.security.Key;
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import java.util.Optional;
22+
23+
/**
24+
* A HTTP cookie session store using JSON Web Token. Usage:
25+
* <pre>{@code
26+
* {
27+
* install(new JwtSession());
28+
* }
29+
* }</pre>
30+
*
31+
* It uses <code>HMAC-SHA-256</code> for signing the cookie. Secret key and cookie option can be
32+
* specify programmatically or in your application configuration file.
33+
*
34+
* @author edgar
35+
* @since 2.2.0
36+
*/
37+
public class JwtSession implements Extension {
38+
39+
private final Key key;
40+
41+
private final Cookie cookie;
42+
43+
/**
44+
* Creates a JSON Web Token session store. The <code>session.secret</code> property must be
45+
* defined in your application configuration.
46+
*
47+
* Cookie details are created from <code>session.cookie</code> when present, otherwise uses
48+
* {@link SessionToken#SID}.
49+
*/
50+
public JwtSession() {
51+
this.key = null;
52+
this.cookie = null;
53+
}
54+
55+
/**
56+
* Creates a JSON Web Token session store. It uses the provided key unless the
57+
* <code>session.secret</code> property is present in your application configuration.
58+
*
59+
* Cookie details are created from <code>session.cookie</code> when present, otherwise uses
60+
* {@link SessionToken#SID}.
61+
*
62+
* @param key Key to use. Override it by <code>session.secret</code> property.
63+
*/
64+
public JwtSession(@Nonnull String key) {
65+
this(key, SessionToken.SID);
66+
}
67+
68+
/**
69+
* Creates a JSON Web Token session store. It uses the provided key unless the
70+
* <code>session.secret</code> property is present in your application configuration.
71+
*
72+
* Cookie details are created from <code>session.cookie</code> when present, otherwise uses
73+
* the provided cookie.
74+
*
75+
* See {@link Cookie#create(String, Config)}.
76+
*
77+
* @param key Key to use. Override it by <code>session.secret</code> property.
78+
* @param cookie Cookie to use. Override it by <code>session.cookie</code> property.
79+
*/
80+
public JwtSession(@Nonnull String key, @Nonnull Cookie cookie) {
81+
this(Keys.hmacShaKeyFor(key.getBytes()), cookie);
82+
}
83+
84+
/**
85+
* Creates a JSON Web Token session store. It uses the provided key unless the
86+
* <code>session.secret</code> property is present in your application configuration.
87+
*
88+
* Cookie details are created from <code>session.cookie</code> when present, otherwise uses
89+
* the provided cookie.
90+
*
91+
* See {@link Cookie#create(String, Config)}.
92+
*
93+
* @param key Key to use. Override it by <code>session.secret</code> property.
94+
* @param cookie Cookie to use. Override it by <code>session.cookie</code> property.
95+
*/
96+
public JwtSession(@Nonnull Key key, @Nonnull Cookie cookie) {
97+
this.key = key;
98+
this.cookie = cookie;
99+
}
100+
101+
@Override public void install(@Nonnull Jooby application) throws Exception {
102+
Config config = application.getConfig();
103+
Key key = config.hasPath("session.secret")
104+
? Keys.hmacShaKeyFor(config.getString("session.secret").getBytes())
105+
: this.key;
106+
if (key == null) {
107+
throw new IllegalStateException("No secret session secret key");
108+
}
109+
Cookie cookie = Cookie.create("session.cookie", config)
110+
.orElse(Optional.ofNullable(this.cookie).orElse(SessionToken.SID));
111+
112+
application.setSessionStore(SessionStore.cookie(cookie, decoder(key), encoder(key)));
113+
}
114+
115+
static SneakyThrows.Function<String, Map<String, String>> decoder(Key key) {
116+
return value -> {
117+
try {
118+
Jws<Claims> claims = Jwts.parser().setSigningKey(key).parseClaimsJws(value);
119+
Map<String, String> attributes = new HashMap<>();
120+
for (Map.Entry<String, Object> entry : claims.getBody().entrySet()) {
121+
attributes.put(entry.getKey(), entry.getValue().toString());
122+
}
123+
return attributes;
124+
} catch (JwtException x) {
125+
return null;
126+
}
127+
};
128+
}
129+
130+
static SneakyThrows.Function<Map<String, String>, String> encoder(Key key) {
131+
return attributes -> {
132+
JwtBuilder builder = Jwts.builder().signWith(key);
133+
for (Map.Entry<String, String> entry : attributes.entrySet()) {
134+
builder.claim(entry.getKey(), entry.getValue());
135+
}
136+
return builder.compact();
137+
};
138+
}
139+
}

0 commit comments

Comments
 (0)