Skip to content

Commit d8a54c5

Browse files
committed
Multiclient auth+100% coverage
1 parent 014bda6 commit d8a54c5

6 files changed

Lines changed: 171 additions & 55 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.jooby.pac4j;
2+
3+
import org.jooby.test.ServerFeature;
4+
import org.junit.Test;
5+
import org.pac4j.core.exception.CredentialsException;
6+
import org.pac4j.core.profile.UserProfile;
7+
import org.pac4j.http.client.direct.DirectBasicAuthClient;
8+
import org.pac4j.http.client.direct.HeaderClient;
9+
import org.pac4j.http.credentials.HttpCredentials;
10+
import org.pac4j.http.credentials.TokenCredentials;
11+
import org.pac4j.http.credentials.authenticator.Authenticator;
12+
import org.pac4j.http.credentials.authenticator.test.SimpleTestUsernamePasswordAuthenticator;
13+
import org.pac4j.http.profile.HttpProfile;
14+
import org.pac4j.http.profile.creator.AuthenticatorProfileCreator;
15+
16+
import com.google.common.io.BaseEncoding;
17+
18+
public class MultipleClientOnSameUrlFeature extends ServerFeature {
19+
20+
public static class HeaderAuthenticator implements Authenticator<TokenCredentials> {
21+
22+
@Override
23+
public void validate(final TokenCredentials credentials) {
24+
if (credentials == null || !credentials.getToken().equals("1234")) {
25+
throw new CredentialsException("Bad token");
26+
}
27+
}
28+
29+
}
30+
31+
{
32+
33+
HeaderClient client = new HeaderClient();
34+
client.setHeaderName("X-Token");
35+
client.setAuthenticator(new HeaderAuthenticator());
36+
client.setProfileCreator(credentials -> {
37+
HttpProfile profile = new HttpProfile();
38+
profile.setId(credentials.getToken());
39+
return profile;
40+
});
41+
use(new Auth()
42+
.client("/multi-client/**", client)
43+
.client("/multi-client/**", new DirectBasicAuthClient(
44+
new SimpleTestUsernamePasswordAuthenticator(),
45+
new AuthenticatorProfileCreator<HttpCredentials, UserProfile>())));
46+
47+
get("/multi-client", req -> req.get(Auth.CNAME));
48+
}
49+
50+
@Test
51+
public void auth() throws Exception {
52+
request()
53+
.get("/multi-client")
54+
.header("X-Token", "1234")
55+
.expect("HeaderClient")
56+
.expect(200);
57+
}
58+
59+
@Test
60+
public void basic() throws Exception {
61+
request()
62+
.get("/multi-client")
63+
.header("Authorization", "Basic " + BaseEncoding.base64().encode("test:test".getBytes()))
64+
.expect("DirectBasicAuthClient")
65+
.expect(200);
66+
}
67+
68+
@Test
69+
public void unauthorized() throws Exception {
70+
request()
71+
.get("/multi-client")
72+
.expect(401);
73+
}
74+
75+
}

jooby-pac4j/src/main/java/org/jooby/internal/pac4j/AuthFilter.java

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static java.util.Objects.requireNonNull;
2222

2323
import java.util.List;
24+
import java.util.function.Predicate;
2425

2526
import org.jooby.Err;
2627
import org.jooby.Request;
@@ -43,8 +44,13 @@
4344
import org.slf4j.Logger;
4445
import org.slf4j.LoggerFactory;
4546

47+
import javaslang.CheckedFunction1;
48+
4649
public class AuthFilter implements Route.Handler {
4750

51+
@SuppressWarnings("rawtypes")
52+
private static final Predicate<Client> useSession = c -> c instanceof IndirectClient;
53+
4854
/** The logging system. */
4955
private final Logger log = LoggerFactory.getLogger(getClass());
5056

@@ -67,54 +73,62 @@ public String getName() {
6773
return clientName;
6874
}
6975

70-
@SuppressWarnings({"rawtypes", "unchecked" })
76+
@SuppressWarnings({"unchecked" })
7177
@Override
72-
public void handle(final Request req, final Response rsp) throws Exception {
78+
public void handle(final Request req, final Response rsp) throws Throwable {
7379
Clients clients = req.require(Clients.class);
7480
String clientName = req.param(clients.getClientNameParameter()).value(this.clientName);
7581

7682
WebContext ctx = req.require(WebContext.class);
7783
ClientFinder finder = req.require(ClientFinder.class);
7884
AuthStore<UserProfile> store = req.require(AuthStore.class);
7985

80-
Client client = find(finder, clients, ctx, null, clientName);
81-
82-
boolean useSession = client instanceof IndirectClient;
83-
84-
String profileId = profileID(useSession, req);
85-
UserProfile profile = profileId == null ? null : store.get(profileId).orElse(null);
86-
87-
if (profile == null) {
88-
if (client instanceof DirectClient) {
89-
log.debug("Performing authentication for client: {}", client);
90-
try {
91-
Credentials credentials = client.getCredentials(ctx);
92-
log.debug("credentials: {}", credentials);
93-
profile = client.getUserProfile(credentials, ctx);
94-
log.debug("profile: {}", profile);
95-
if (profile != null) {
96-
req.set(Auth.ID, profile.getId());
97-
store.set(profile);
86+
// stateless or previously authenticated stateful
87+
UserProfile profile = find(finder, clients, ctx, null, clientName, client -> {
88+
String profileId = profileID(useSession.test(client), req);
89+
UserProfile identity = profileId == null ? null : store.get(profileId).orElse(null);
90+
91+
if (identity == null) {
92+
if (client instanceof DirectClient) {
93+
log.debug("Performing authentication for client: {}", client);
94+
try {
95+
Credentials credentials = client.getCredentials(ctx);
96+
log.debug("credentials: {}", credentials);
97+
identity = client.getUserProfile(credentials, ctx);
98+
log.debug("profile: {}", identity);
99+
if (identity != null) {
100+
req.set(Auth.ID, identity.getId());
101+
req.set(Auth.CNAME, client.getName());
102+
store.set(identity);
103+
}
104+
} catch (RequiresHttpAction e) {
105+
throw new TechnicalException("Unexpected HTTP action", e);
98106
}
99-
} catch (RequiresHttpAction e) {
100-
throw new TechnicalException("Unexpected HTTP action", e);
101107
}
102108
}
103-
}
109+
return identity;
110+
});
104111

105112
if (profile == null) {
106-
if (useSession) {
107-
// indirect client, start authentication
108-
try {
109-
final String requestedUrl = ctx.getFullRequestURL();
110-
log.debug("requestedUrl: {}", requestedUrl);
111-
ctx.setSessionAttribute(Pac4jConstants.REQUESTED_URL, requestedUrl);
112-
client.redirect(ctx, true);
113-
rsp.end();
114-
} catch (RequiresHttpAction ex) {
115-
new AuthResponse(rsp).handle(client, ex);
113+
// try stateful auth
114+
Boolean redirected = find(finder, clients, ctx, null, clientName, client -> {
115+
if (useSession.test(client)) {
116+
// indirect client, start authentication
117+
try {
118+
final String requestedUrl = ctx.getFullRequestURL();
119+
log.debug("requestedUrl: {}", requestedUrl);
120+
ctx.setSessionAttribute(Pac4jConstants.REQUESTED_URL, requestedUrl);
121+
client.redirect(ctx, true);
122+
rsp.end();
123+
} catch (RequiresHttpAction ex) {
124+
new AuthResponse(rsp).handle(client, ex);
125+
}
126+
return Boolean.TRUE;
127+
} else {
128+
return null;
116129
}
117-
} else {
130+
});
131+
if (redirected != Boolean.TRUE) {
118132
throw new Err(Status.UNAUTHORIZED);
119133
}
120134
} else {
@@ -129,13 +143,19 @@ private String profileID(final boolean useSession, final Request req) {
129143
}
130144

131145
@SuppressWarnings("rawtypes")
132-
private Client find(final ClientFinder finder, final Clients clients, final WebContext ctx,
133-
final Class<? extends Client<?, ?>> clientType, final String clientName) {
146+
private <T> T find(final ClientFinder finder, final Clients clients, final WebContext ctx,
147+
final Class<? extends Client<?, ?>> clientType, final String clientName,
148+
final CheckedFunction1<Client, T> fn) throws Throwable {
149+
134150
List<Client> result = finder.find(clients, ctx, clientName);
135-
if (result.size() > 0) {
136-
return result.get(0);
151+
for (Client client : result) {
152+
T value = fn.apply(client);
153+
if (value != null) {
154+
return value;
155+
}
137156
}
138-
throw new Err(Status.UNAUTHORIZED);
157+
158+
return null;
139159
}
140160

141161
@SuppressWarnings("rawtypes")

jooby-pac4j/src/main/java/org/jooby/internal/pac4j/AuthSerializer.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import com.google.common.io.BaseEncoding;
2727
import com.google.common.primitives.Primitives;
2828

29+
import javaslang.control.Try;
30+
2931
public final class AuthSerializer {
3032

3133
private static final String PREFIX = "b64~";
@@ -34,26 +36,24 @@ public static final Object strToObject(final String value) {
3436
if (value == null || !value.startsWith(PREFIX)) {
3537
return value;
3638
}
37-
byte[] bytes = BaseEncoding.base64().decode(value.substring(PREFIX.length()));
38-
try (ObjectInputStream stream = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
39-
return stream.readObject();
40-
} catch (Exception ex) {
41-
throw new IllegalArgumentException("Can't de-serialize value " + value, ex);
42-
}
39+
return Try.of(() -> {
40+
byte[] bytes = BaseEncoding.base64().decode(value.substring(PREFIX.length()));
41+
return new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject();
42+
}).getOrElseThrow(
43+
ex -> new IllegalArgumentException("Can't de-serialize value " + value, ex));
4344
}
4445

4546
public static final String objToStr(final Object value) {
4647
if (value instanceof CharSequence || Primitives.isWrapperType(value.getClass())) {
4748
return value.toString();
4849
}
49-
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
50-
try (ObjectOutputStream stream = new ObjectOutputStream(bytes)) {
50+
return Try.of(() -> {
51+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
52+
ObjectOutputStream stream = new ObjectOutputStream(bytes);
5153
stream.writeObject(value);
5254
stream.flush();
5355
return PREFIX + BaseEncoding.base64().encode(bytes.toByteArray());
54-
} catch (Exception ex) {
55-
throw new IllegalArgumentException("Can't serialize value " + value, ex);
56-
}
56+
}).getOrElseThrow(ex -> new IllegalArgumentException("Can't serialize value " + value, ex));
5757
}
5858

5959
}

jooby-pac4j/src/main/java/org/jooby/pac4j/Auth.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,8 @@ public class Auth implements Jooby.Module {
288288
/** Name of the local request variable that holds the username. */
289289
public static final String ID = Auth.class.getName() + ".id";
290290

291+
public static final String CNAME = Auth.class.getName() + ".client.id";
292+
291293
private Multimap<String, BiFunction<Binder, Config, AuthFilter>> bindings = ArrayListMultimap
292294
.create();
293295

jooby-pac4j/src/test/java/org/jooby/internal/pac4j/AuthFilterTest.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import org.powermock.core.classloader.annotations.PrepareForTest;
3434
import org.powermock.modules.junit4.PowerMockRunner;
3535

36+
import javaslang.control.Try;
37+
3638
@RunWith(PowerMockRunner.class)
3739
@PrepareForTest({AuthFilter.class, Clients.class })
3840
public class AuthFilterTest {
@@ -67,8 +69,12 @@ public class AuthFilterTest {
6769
@SuppressWarnings("rawtypes")
6870
private <C extends Client> Block findClient(final Class<C> clientType, final String client) {
6971
return unit -> {
70-
C c = unit.mock(clientType);
71-
unit.registerMock(clientType, c);
72+
C c = Try.of(() -> unit.get(clientType))
73+
.getOrElse(() -> {
74+
C m = unit.mock(clientType);
75+
unit.registerMock(clientType, m);
76+
return m;
77+
});
7278

7379
ClientFinder finder = unit.get(ClientFinder.class);
7480
expect(finder.find(unit.get(Clients.class), unit.get(WebContext.class), client))
@@ -201,6 +207,7 @@ public void handleDirect() throws Exception {
201207
.expect(clientName("ParameterClient"))
202208
.expect(findClient(ParameterClient.class, "ParameterClient"))
203209
.expect(reqAuthID(profileId))
210+
.expect(reqAuthCNAME(ParameterClient.class))
204211
.expect(authStore(profileId, null))
205212
.expect(creds(ParameterClient.class))
206213
.expect(userProfile(ParameterClient.class, profile))
@@ -217,6 +224,17 @@ public void handleDirect() throws Exception {
217224
});
218225
}
219226

227+
@SuppressWarnings("rawtypes")
228+
private Block reqAuthCNAME(final Class<? extends Client> client) {
229+
return unit -> {
230+
Client c = unit.get(client);
231+
expect(c.getName()).andReturn(client.getSimpleName());
232+
233+
Request req = unit.get(Request.class);
234+
expect(req.set(Auth.CNAME, client.getSimpleName())).andReturn(req);
235+
};
236+
}
237+
220238
@Test
221239
public void handleDirectNoClient() throws Exception {
222240
String profileId = "123";
@@ -231,11 +249,11 @@ public void handleDirectNoClient() throws Exception {
231249
.expect(clients)
232250
.expect(clientName("ParameterClient"))
233251
.expect(findNoClient(ParameterClient.class, "ParameterClient"))
252+
.expect(findNoClient(ParameterClient.class, "ParameterClient"))
234253
.run(unit -> {
235254
try {
236255
new AuthFilter(ParameterClient.class, HttpProfile.class)
237-
.handle(unit.get(Request.class), unit.get(Response.class),
238-
unit.get(Route.Chain.class));
256+
.handle(unit.get(Request.class), unit.get(Response.class));
239257
fail("expecting 401");
240258
} catch (Err ex) {
241259
assertEquals(401, ex.statusCode());
@@ -280,6 +298,7 @@ public void handleDirectNoProfile() throws Exception {
280298
.expect(clients)
281299
.expect(clientName("ParameterClient"))
282300
.expect(findClient(ParameterClient.class, "ParameterClient"))
301+
.expect(findClient(ParameterClient.class, "ParameterClient"))
283302
.expect(reqAuthID(profileId))
284303
.expect(authStore(profileId, null))
285304
.expect(creds(ParameterClient.class))

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2349,7 +2349,7 @@ org.eclipse.jdt.apt.processorOptions/defaultOverwrite=true
23492349
<junit.version>4.11</junit.version>
23502350
<easymock.version>3.2</easymock.version>
23512351
<powermock.version>1.6.4</powermock.version>
2352-
<jacoco.version>0.7.5.201505241946</jacoco.version>
2352+
<jacoco.version>0.7.7.201606060606</jacoco.version>
23532353

23542354
<!-- Maven properties -->
23552355
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

0 commit comments

Comments
 (0)