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 21 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 @@ -23,6 +23,7 @@
import java.util.function.ToLongFunction;

import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.reaktivity.nukleus.Elektron;
import org.reaktivity.nukleus.oauth.internal.stream.OAuthProxyFactoryBuilder;
import org.reaktivity.nukleus.route.RouteKind;
Expand All @@ -34,7 +35,7 @@ final class OAuthElektron implements Elektron

OAuthElektron(
Function<String, JsonWebKey> supplyKey,
ToLongFunction<String> resolveRealm)
ToLongFunction<JsonWebSignature> resolveRealm)
{
this.streamFactoryBuilders = singletonMap(PROXY, new OAuthProxyFactoryBuilder(supplyKey, resolveRealm));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
package org.reaktivity.nukleus.oauth.internal;

import java.nio.file.Path;
import java.util.LinkedList;
import java.util.List;

import org.agrona.DirectBuffer;
import org.agrona.MutableDirectBuffer;
import org.agrona.collections.Int2ObjectHashMap;
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 All @@ -33,6 +37,8 @@ final class OAuthNukleus implements Nukleus
{
static final String NAME = "oauth";

public static final String[] EMPTY_STRING_ARRAY = new String[0];
dknoma marked this conversation as resolved.
Show resolved Hide resolved

private final ResolveFW resolveRO = new ResolveFW();
private final ResolvedFW.Builder resolvedRW = new ResolvedFW.Builder();
private final UnresolveFW unresolveRO = new UnresolveFW();
Expand Down Expand Up @@ -81,7 +87,7 @@ public CommandHandler commandHandler(
@Override
public OAuthElektron supplyElektron()
{
return new OAuthElektron(realms::supplyKey, realms::resolve);
return new OAuthElektron(realms::supplyKey, realms::lookup);
}

private void onResolve(
Expand All @@ -95,7 +101,11 @@ private void onResolve(
final long correlationId = resolve.correlationId();
final String realm = resolve.realm().asString();

long authorization = realms.resolve(realm);
final ListFW<StringFW> roles = resolve.roles();
final List<String> collectedRoles = new LinkedList<>();
roles.forEach(r -> collectedRoles.add(r.asString()));
final long authorization = realms.resolve(realm, collectedRoles.toArray(EMPTY_STRING_ARRAY));

if (authorization != 0L)
{
final ResolvedFW resolved = resolvedRW.wrap(replyBuffer, 0, replyBuffer.capacity())
Expand Down
118 changes: 112 additions & 6 deletions src/main/java/org/reaktivity/nukleus/oauth/internal/OAuthRealms.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,25 @@

import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKeySet;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.lang.JoseException;
import org.reaktivity.nukleus.internal.CopyOnWriteHashMap;

public class OAuthRealms
{
public static final String[] EMPTY_STRING_ARRAY = new String[0];
dknoma marked this conversation as resolved.
Show resolved Hide resolved

private static final String SCOPES_CLAIM = "scopes";
dknoma marked this conversation as resolved.
Show resolved Hide resolved
private static final Long NO_AUTHORIZATION = 0L;

// To optimize authorization checks we use a single distinct bit per realm
private static final int MAX_REALMS = Short.SIZE;

private static final long SCOPE_MASK = 0xFFFF_000000000000L;

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

private int nextRealmBitShift = 48;

Expand All @@ -66,7 +72,8 @@ public OAuthRealms(
private OAuthRealms(
Map<String, JsonWebKey> keysByKid)
{
keysByKid.forEach((k, v) -> add(v.getKeyId()));
// Moved realm assignment from startup to the first RESOLVE call
// keysByKid.forEach((k, v) -> add(v.getKeyId()));
dknoma marked this conversation as resolved.
Show resolved Hide resolved
this.keysByKid = keysByKid;
}

Expand All @@ -77,13 +84,70 @@ public void add(
{
throw new IllegalStateException("Too many realms");
}
realmsIdsByName.put(realm, 1L << nextRealmBitShift++);
realmsIdsByName.put(realm, new OAuthRealm(1L << nextRealmBitShift++));
}

public long resolve(
String realm)
String realm,
String[] scopes)
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
dknoma marked this conversation as resolved.
Show resolved Hide resolved
return realmsIdsByName.getOrDefault(realm, NO_AUTHORIZATION);
// If realm doesn't exist, add it to realms
if(!realmsIdsByName.containsKey(realm))
{
add(realm);
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}
final OAuthRealm realmObject = realmsIdsByName.get(realm);
dknoma marked this conversation as resolved.
Show resolved Hide resolved
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 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.supplyScopeBit(scope))
{
throw new IllegalStateException("Too many scopes");
}
final long bit = realmObject.getScopeBit(scope);
realmBit |= bit;
}
return realmBit;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}

public long lookup(
JsonWebSignature verified)
{
final String realmName = verified.getKeyIdHeaderValue();
try
{
final JwtClaims claims = JwtClaims.parse(verified.getPayload());
final Object scopeClaim = claims.getClaimValue(SCOPES_CLAIM);
final String[] roleNames = scopeClaim != null ?
splitScopes(scopeClaim.toString())
: EMPTY_STRING_ARRAY;
final OAuthRealm realm = realmsIdsByName.get(realmName);
if(realm == null)
{
return NO_AUTHORIZATION;
}
long authorizationBits = realm.realmBit;
for (int i = 0; i < roleNames.length; i++)
{
final String scope = roleNames[i];
final long bit = realm.getScopeBit(scope);
authorizationBits |= bit;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}
return authorizationBits;
}
catch (JoseException | InvalidJwtException e)
{
// TODO: diagnostics?
return NO_AUTHORIZATION;
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}

public boolean unresolve(
Expand All @@ -97,7 +161,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 All @@ -108,6 +172,12 @@ public JsonWebKey supplyKey(
return keysByKid.get(kid);
}

private String[] splitScopes(
String scopes)
{
return scopes.split("\\s+");
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
dknoma marked this conversation as resolved.
Show resolved Hide resolved

private static Map<String, JsonWebKey> parseKeyMap(
Path keyFile)
{
Expand Down Expand Up @@ -167,4 +237,40 @@ private static Map<String, JsonWebKey> toKeyMap(

return keysByKid;
}

private final class OAuthRealm
{
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 OAuthRealm(
long realmBit)
{
this.realmBit = realmBit;
}

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

private boolean supplyScopeBit(
String scope)
{
// return true if not reach scope cap and scope bit >= 0
return scopeBitsByName.size() < MAX_SCOPES &&
scopeBitsByName.computeIfAbsent(scope, s -> 1L << nextScopeBitShift++) >= 0;
}

private long getScopeBit(
String scope)
{
return scopeBitsByName.getOrDefault(scope, 0L);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,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 ToLongFunction<JsonWebSignature> resolveRealm;
private final SignalingExecutor executor;

private final Long2ObjectHashMap<OAuthProxy> correlations;
Expand All @@ -106,7 +106,7 @@ public OAuthProxyFactory(
LongUnaryOperator supplyReplyId,
Long2ObjectHashMap<OAuthProxy> correlations,
Function<String, JsonWebKey> supplyKey,
ToLongFunction<String> resolveRealm,
ToLongFunction<JsonWebSignature> resolveRealm,
SignalingExecutor executor)
{
this.router = requireNonNull(router);
Expand Down Expand Up @@ -155,14 +155,12 @@ private MessageConsumer newInitialStream(
long connectAuthorization = acceptAuthorization;
if (verified != null)
{
final String kid = verified.getKeyIdHeaderValue();
connectAuthorization = resolveRealm.applyAsLong(kid);
connectAuthorization = resolveRealm.applyAsLong(verified);
}

final long acceptRouteId = begin.routeId();
final MessagePredicate filter = (t, b, o, l) -> true;
final RouteFW route = router.resolve(acceptRouteId, connectAuthorization, filter, this::wrapRoute);

MessageConsumer newStream = null;

if (route != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,31 @@
*/
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 org.agrona.MutableDirectBuffer;
import org.agrona.collections.Long2ObjectHashMap;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jws.JsonWebSignature;
import org.reaktivity.nukleus.buffer.BufferPool;
import org.reaktivity.nukleus.concurrent.SignalingExecutor;
import org.reaktivity.nukleus.oauth.internal.stream.OAuthProxyFactory.OAuthProxy;
import org.reaktivity.nukleus.route.RouteManager;
import org.reaktivity.nukleus.stream.StreamFactory;
import org.reaktivity.nukleus.stream.StreamFactoryBuilder;

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;

dknoma marked this conversation as resolved.
Show resolved Hide resolved
public class OAuthProxyFactoryBuilder implements StreamFactoryBuilder
{
private final Function<String, JsonWebKey> supplyKey;
private final ToLongFunction<String> resolveRealm;
private final ToLongFunction<JsonWebSignature> resolveRealm;
// private final ToLongBiFunction<String, String[]> resolveRealm;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
private final Long2ObjectHashMap<OAuthProxy> correlations;

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

public OAuthProxyFactoryBuilder(
Function<String, JsonWebKey> supplyKey,
ToLongFunction<String> resolveRealm)
ToLongFunction<JsonWebSignature> resolveRealm)
// ToLongBiFunction<String, String[]> resolveRealm)
dknoma marked this conversation as resolved.
Show resolved Hide resolved
{
this.supplyKey = supplyKey;
this.resolveRealm = resolveRealm;
Expand Down
Loading