Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce scope claim as authorization bitmask #21

Merged
merged 33 commits into from
Jun 11, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d898a13
cloned tests to test scope for oauth2
dknoma May 25, 2019
387a3a6
changed resolve() references throughout OAuth from a ToLongFunction t…
dknoma May 28, 2019
d320adb
lazily assigned bits to the scopes based off of how many scopes are p…
dknoma May 29, 2019
a3de538
cleaned up code. TODO: need to check for scopes length >48, what http…
dknoma May 30, 2019
96ab6ff
revised resolve(). no uses an OAuthRealmObject to contain realm and s…
dknoma May 30, 2019
ce5f656
removed unnecessary imports from files.
dknoma May 30, 2019
d1bc444
fixed spec test. modular to allow for scopes to be passed as a prop f…
dknoma May 30, 2019
0dc65ae
fixed pom.xml. using correct dependencies now. also fixed an error in…
dknoma May 31, 2019
30d77cf
passing all StreamsIT and ControllerIT tests. fixed another empty rol…
dknoma May 31, 2019
354c63c
working on getting test scripts to work together. doesnt seem to reco…
dknoma May 31, 2019
e8963bf
WithNoSetScopes and WithSetScopes tests are passing. Working on WithE…
dknoma May 31, 2019
3b47c41
Working on WithExtraScope tests.
dknoma Jun 3, 2019
2f4ce8b
fixed checkstyle. commented out extra scopes test to pass build
dknoma Jun 3, 2019
f7f6ffb
cleaned up code. extra scopes test should now be passing correctly. s…
dknoma Jun 3, 2019
9224617
made changes from the requests: removed unnecessary imports and lefto…
dknoma Jun 4, 2019
baafabf
builds successfully now. Need to change OAuthRealmsTests as they use …
dknoma Jun 4, 2019
941d2a8
Also moved realm assignment from startup to first Resolve. test corre…
dknoma Jun 4, 2019
13e5d8f
commented out test code. need to figure out a way to get the test to …
dknoma Jun 4, 2019
f585e8e
fixed shouldNotResolveUnkownRealm() test: was accidentally adding a r…
dknoma Jun 4, 2019
8820539
OAuthRealmsTest now works with the new lookup(). All tests passing, b…
dknoma Jun 5, 2019
23299bf
removed commented out code and print statements and TODOs.
dknoma Jun 5, 2019
6af563a
made fixes to the requested changes. removed add(), changed OAuthReal…
dknoma Jun 6, 2019
b772de8
optimized OAuthRealm. No longer uses redundant methods: removed old g…
dknoma Jun 6, 2019
7cfc640
changed the way realm bit shift works to be more consistent with the …
dknoma Jun 6, 2019
017aa7d
fixed MAX_SCOPES check in resolve(). also created a helper method in …
dknoma Jun 7, 2019
f0e40e0
fixed ControllerIT tests to assert the correct authorization bits. ap…
dknoma Jun 7, 2019
1d1346a
changed variable names to be consistent with their function and purpo…
dknoma Jun 7, 2019
29af370
changed variable names to be consistent with their function and purpo…
dknoma Jun 7, 2019
a5b20ca
set up previously unimplemented tests. also created and updated a few…
dknoma Jun 10, 2019
1f71897
set up previously unimplemented tests. also created and updated a few…
dknoma Jun 10, 2019
eb07cf9
removed throwing IllegalStateException. instead of throwing an error,…
dknoma Jun 10, 2019
5a18aaf
removed second parameter from `OAuthRealm` constructor. Changed `this…
dknoma Jun 10, 2019
c9ee38e
Update nukleus-oauth.spec dependency version
jfallows Jun 11, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@

<nukleus.plugin.version>0.33</nukleus.plugin.version>

<nukleus.oauth.spec.version>0.21</nukleus.oauth.spec.version>
<nukleus.oauth.spec.version>develop-SNAPSHOT</nukleus.oauth.spec.version>
<reaktor.version>0.92</reaktor.version>
</properties>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

import java.util.Map;
import java.util.function.Function;
import java.util.function.ToLongFunction;
import java.util.function.ToLongBiFunction;

import org.jose4j.jwk.JsonWebKey;
import org.reaktivity.nukleus.Elektron;
Expand All @@ -34,7 +34,7 @@ final class OAuthElektron implements Elektron

OAuthElektron(
Function<String, JsonWebKey> supplyKey,
ToLongFunction<String> resolveRealm)
ToLongBiFunction<String, String[]> resolveRealm)
{
this.streamFactoryBuilders = singletonMap(PROXY, new OAuthProxyFactoryBuilder(supplyKey, resolveRealm));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.reaktivity.nukleus.Nukleus;
import org.reaktivity.nukleus.function.CommandHandler;
import org.reaktivity.nukleus.function.MessageConsumer;
import org.reaktivity.nukleus.oauth.internal.types.ListFW;
import org.reaktivity.nukleus.oauth.internal.types.StringFW;
import org.reaktivity.nukleus.oauth.internal.types.control.ErrorFW;
import org.reaktivity.nukleus.oauth.internal.types.control.auth.ResolveFW;
import org.reaktivity.nukleus.oauth.internal.types.control.auth.ResolvedFW;
Expand Down Expand Up @@ -95,7 +97,19 @@ private void onResolve(
final long correlationId = resolve.correlationId();
final String realm = resolve.realm().asString();

long authorization = realms.resolve(realm);
final ListFW<StringFW> rolesRO = resolve.roles();
dknoma marked this conversation as resolved.
Show resolved Hide resolved
final long authorization;
if(rolesRO.isEmpty())
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
authorization = realms.resolveAndPutIfAbsent(realm, null);
}
else
{
final StringBuilder rolesBuilder = new StringBuilder();
rolesRO.forEach(role -> rolesBuilder.append(role.asString()).append(" "));
authorization = realms.resolveAndPutIfAbsent(realm, rolesBuilder.toString().split("\\s+"));
jfallows marked this conversation as resolved.
Show resolved Hide resolved
}

if (authorization != 0L)
{
final ResolvedFW resolved = resolvedRW.wrap(replyBuffer, 0, replyBuffer.capacity())
Expand Down
106 changes: 98 additions & 8 deletions src/main/java/org/reaktivity/nukleus/oauth/internal/OAuthRealms.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;
dknoma marked this conversation as resolved.
Show resolved Hide resolved

import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKeySet;
Expand All @@ -40,7 +38,7 @@ public class OAuthRealms

private static final long SCOPE_MASK = 0xFFFF_000000000000L;

private final Map<String, Long> realmsIdsByName = new CopyOnWriteHashMap<>();
private final Map<String, OAuthRealmObject> realmsIdsByName = new CopyOnWriteHashMap<>();

private int nextRealmBitShift = 48;

Expand Down Expand Up @@ -77,13 +75,66 @@ public void add(
{
throw new IllegalStateException("Too many realms");
}
realmsIdsByName.put(realm, 1L << nextRealmBitShift++);
realmsIdsByName.put(realm, new OAuthRealmObject(1L << nextRealmBitShift++));
}

public long resolveAndPutIfAbsent(
String realm,
String[] scopes)
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
dknoma marked this conversation as resolved.
Show resolved Hide resolved
final OAuthRealmObject realmObject = realmsIdsByName.get(realm);
if(realmObject == null)
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
return NO_AUTHORIZATION;
}
jfallows marked this conversation as resolved.
Show resolved Hide resolved
dknoma marked this conversation as resolved.
Show resolved Hide resolved
long realmBit = realmObject.realmBit;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
if(scopes == null || scopes.length <= 0)
{
return realmBit;
}
// if not already there, add the scope to the map, assign each scope a bit
// which determines which low bit will be flipped if that scope is present
for (int i = 0; i < scopes.length; i++)
{
final String scope = scopes[i];
// check if scope's bit has been set and if scope can be added
if(!realmObject.scopeBitAssigned(scope) && !realmObject.addScopeBit(scope))
{
throw new IllegalStateException("Too many scopes");
}
final long bit = realmObject.getScopeBit(scope);
if(bit >= 0)
{
realmBit |= bit;
}
}
return realmBit;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}

public long resolve(
String realm)
String realm,
String[] scopes)
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
return realmsIdsByName.getOrDefault(realm, NO_AUTHORIZATION);
final OAuthRealmObject realmObject = realmsIdsByName.get(realm);
dknoma marked this conversation as resolved.
Show resolved Hide resolved
if(realmObject == null)
{
return NO_AUTHORIZATION;
}
long authorizationBits = realmObject.realmBit;
if(scopes == null || scopes.length <= 0)
{
return authorizationBits;
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
for (int i = 0; i < scopes.length; i++)
{
final String scope = scopes[i];
final long bit = realmObject.getScopeBit(scope);
if(bit >= 0)
{
authorizationBits |= bit;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
return authorizationBits;
}

public boolean unresolve(
Expand All @@ -97,7 +148,7 @@ public boolean unresolve(
}
else
{
result = realmsIdsByName.entrySet().removeIf(e -> (e.getValue() == scope));
result = realmsIdsByName.entrySet().removeIf(e -> (e.getValue().realmBit == scope));
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}
return result;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}
Expand Down Expand Up @@ -167,4 +218,43 @@ private static Map<String, JsonWebKey> toKeyMap(

return keysByKid;
}

private final class OAuthRealmObject
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
private static final int MAX_SCOPES = 48;

private final Map<String, Long> scopeBitsByName = new CopyOnWriteHashMap<>();

private long realmBit;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
private long nextScopeBitShift;
dknoma marked this conversation as resolved.
Show resolved Hide resolved

private OAuthRealmObject(long realmBit)
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
this.realmBit = realmBit;
}

private boolean scopeBitAssigned(
String scope)
{
return scopeBitsByName.containsKey(scope);
}

private boolean addScopeBit(
String scope)
{
final long nextScopeBit = scopeBitsByName.size() < MAX_SCOPES ? (1L << nextScopeBitShift++) : -1;
if(nextScopeBit < 0)
{
return false;
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
scopeBitsByName.put(scope, nextScopeBit);
return true;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}

private long getScopeBit(
String scope)
{
return scopeBitsByName.getOrDefault(scope, -1L);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
import static org.reaktivity.nukleus.oauth.internal.util.BufferUtil.indexOfBytes;

import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.function.LongSupplier;
import java.util.function.LongUnaryOperator;
import java.util.function.ToLongFunction;
import java.util.function.*;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand Down Expand Up @@ -59,6 +56,8 @@

public class OAuthProxyFactory implements StreamFactory
{
private static final String SCOPES_CLAIM = "scopes";

private static final long EXPIRES_NEVER = Long.MAX_VALUE;
private static final long EXPIRES_IMMEDIATELY = 0L;

Expand Down Expand Up @@ -90,7 +89,7 @@ public class OAuthProxyFactory implements StreamFactory
private final LongSupplier supplyTrace;
private final LongUnaryOperator supplyReplyId;
private final Function<String, JsonWebKey> supplyKey;
private final ToLongFunction<String> resolveRealm;
private final ToLongBiFunction<String, String[]> resolveRealm;
private final SignalingExecutor executor;

private final Long2ObjectHashMap<OAuthProxy> correlations;
Expand All @@ -106,7 +105,7 @@ public OAuthProxyFactory(
LongUnaryOperator supplyReplyId,
Long2ObjectHashMap<OAuthProxy> correlations,
Function<String, JsonWebKey> supplyKey,
ToLongFunction<String> resolveRealm,
ToLongBiFunction<String, String[]> resolveRealm,
SignalingExecutor executor)
{
this.router = requireNonNull(router);
Expand Down Expand Up @@ -145,6 +144,12 @@ public MessageConsumer newStream(
return newStream;
}

private String[] splitScopes(
String scopes)
{
return scopes.split("\\s+");
}

private MessageConsumer newInitialStream(
final BeginFW begin,
final MessageConsumer acceptReply)
Expand All @@ -155,8 +160,20 @@ private MessageConsumer newInitialStream(
long connectAuthorization = acceptAuthorization;
if (verified != null)
{
final String kid = verified.getKeyIdHeaderValue();
connectAuthorization = resolveRealm.applyAsLong(kid);
// Try to parse the scopes claim if it exists.
try
{
final String kid = verified.getKeyIdHeaderValue();
final JwtClaims claims = JwtClaims.parse(signature.getPayload());
dknoma marked this conversation as resolved.
Show resolved Hide resolved
final Object scopeClaim = claims.getClaimValue(SCOPES_CLAIM);
connectAuthorization = scopeClaim != null ?
resolveRealm.applyAsLong(kid, splitScopes(scopeClaim.toString()))
: resolveRealm.applyAsLong(kid, null);
}
catch (JoseException | InvalidJwtException ex)
{
// TODO: diagnostics?
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}

final long acceptRouteId = begin.routeId();
Expand Down Expand Up @@ -480,6 +497,7 @@ private JsonWebSignature verifiedSignature(
final JwtClaims claims = JwtClaims.parse(signature.getPayload());
final NumericDate expirationTime = claims.getExpirationTime();
final NumericDate notBefore = claims.getNotBefore();
// System.out.println("verifying signature - scopes: "+claims.getClaimValue(SCOPES_CLAIM));
dknoma marked this conversation as resolved.
Show resolved Hide resolved

final long now = System.currentTimeMillis();
if ((expirationTime == null || now <= expirationTime.getValueInMillis()) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,7 @@
*/
package org.reaktivity.nukleus.oauth.internal.stream;

import java.util.function.Function;
import java.util.function.IntUnaryOperator;
import java.util.function.LongFunction;
import java.util.function.LongSupplier;
import java.util.function.LongUnaryOperator;
import java.util.function.Supplier;
import java.util.function.ToLongFunction;
import java.util.function.*;
dknoma marked this conversation as resolved.
Show resolved Hide resolved

import org.agrona.MutableDirectBuffer;
import org.agrona.collections.Long2ObjectHashMap;
Expand All @@ -36,7 +30,7 @@
public class OAuthProxyFactoryBuilder implements StreamFactoryBuilder
{
private final Function<String, JsonWebKey> supplyKey;
private final ToLongFunction<String> resolveRealm;
private final ToLongBiFunction<String, String[]> resolveRealm;
private final Long2ObjectHashMap<OAuthProxy> correlations;

private RouteManager router;
Expand All @@ -48,7 +42,7 @@ public class OAuthProxyFactoryBuilder implements StreamFactoryBuilder

public OAuthProxyFactoryBuilder(
Function<String, JsonWebKey> supplyKey,
ToLongFunction<String> resolveRealm)
ToLongBiFunction<String, String[]> resolveRealm)
{
this.supplyKey = supplyKey;
this.resolveRealm = resolveRealm;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ public void shouldResolveKnownRealms() throws Exception
OAuthRealms realms = new OAuthRealms();
realms.add("realm one");
realms.add("realm two");
assertEquals(0x0001_000000000000L, realms.resolve("realm one"));
assertEquals(0x0002_000000000000L, realms.resolve("realm two"));
assertEquals(0x0001_000000000000L, realms.resolve("realm one", null));
assertEquals(0x0002_000000000000L, realms.resolve("realm two", null));
}

@Test
public void shouldNotResolveUnknownRealm() throws Exception
{
OAuthRealms realms = new OAuthRealms();
assertEquals(0L, realms.resolve("realm one"));
assertEquals(0L, realms.resolve("realm one", null));
}

@Test
Expand All @@ -72,7 +72,7 @@ public void shouldUnresolveAllKnownRealms() throws Exception
}
for (int i=0; i < Short.SIZE; i++)
{
assertTrue(realms.unresolve(realms.resolve("realm" + i)));
assertTrue(realms.unresolve(realms.resolve("realm" + i, null)));

}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,10 @@ public void shouldResolveWithRoles() throws Exception
k3po.start();

long authorization = reaktor.controller(OAuthController.class)
.resolve("RS256", "role1", "role2")
.resolve("RS256", "scope1", "scope2", "scope3")
.get();
assertEquals(0x0001_00000000000cL, authorization);
// System.out.println("authorization: " + authorization);
dknoma marked this conversation as resolved.
Show resolved Hide resolved
assertEquals(0x0001_000000000007L, authorization);

k3po.finish();
}
Expand Down Expand Up @@ -256,9 +257,9 @@ public void shouldUnresolveWithRoles() throws Exception
k3po.start();

long authorization = reaktor.controller(OAuthController.class)
.resolve("RS256", "role1", "role2")
.resolve("RS256", "scope1", "scope2", "scope3")
.get();
assertEquals(0x0001_00000000000cL, authorization);
assertEquals(0x0001_000000000015L, authorization);

reaktor.controller(OAuthController.class)
.unresolve(authorization)
Expand Down
Loading