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 all 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>0.22</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";

private static final String[] EMPTY_STRING_ARRAY = new String[0];

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
128 changes: 104 additions & 24 deletions src/main/java/org/reaktivity/nukleus/oauth/internal/OAuthRealms.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,26 @@

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
{
private static final String[] EMPTY_STRING_ARRAY = new String[0];
private static final String SCOPE_CLAIM = "scope";
private static final Long NO_AUTHORIZATION = 0L;

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

private static final long SCOPE_MASK = 0xFFFF_000000000000L;
private static final long REALM_MASK = 0xFFFF_000000000000L;

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

private int nextRealmBitShift = 48;
private int nextRealmBit = 0;

private final Map<String, JsonWebKey> keysByKid;

Expand All @@ -66,40 +71,56 @@ public OAuthRealms(
private OAuthRealms(
Map<String, JsonWebKey> keysByKid)
{
keysByKid.forEach((k, v) -> add(v.getKeyId()));
this.keysByKid = keysByKid;
}

public void add(
String realm)
public long resolve(
String realmName,
String[] scopeNames)
{
if (realmsIdsByName.size() == MAX_REALMS)
long authorization = NO_AUTHORIZATION;
if(nextRealmBit < MAX_REALMS)
{
throw new IllegalStateException("Too many realms");
final OAuthRealm realm = realmsIdsByName.computeIfAbsent(realmName, OAuthRealm::new);
authorization = realm.resolve(scopeNames);
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
realmsIdsByName.put(realm, 1L << nextRealmBitShift++);
return authorization;
}

public long resolve(
String realm)
String realmName)
{
dknoma marked this conversation as resolved.
Show resolved Hide resolved
return realmsIdsByName.getOrDefault(realm, NO_AUTHORIZATION);
return resolve(realmName, EMPTY_STRING_ARRAY);
}

public boolean unresolve(
long authorization)
public long lookup(
JsonWebSignature verified)
{
long scope = authorization & SCOPE_MASK;
boolean result;
if (Long.bitCount(scope) > 1)
{
result = false;
}
else
final OAuthRealm realm = realmsIdsByName.get(verified.getKeyIdHeaderValue());
long authorization = NO_AUTHORIZATION;
if(realm != null)
{
result = realmsIdsByName.entrySet().removeIf(e -> (e.getValue() == scope));
try
{
final JwtClaims claims = JwtClaims.parse(verified.getPayload());
final Object scopeClaim = claims.getClaimValue(SCOPE_CLAIM);
final String[] scopeNames = scopeClaim != null ?
scopeClaim.toString().split("\\s+")
: EMPTY_STRING_ARRAY;
authorization = realm.lookup(scopeNames);
}
catch (JoseException | InvalidJwtException e)
{
// TODO: diagnostics?
}
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
return result;
return authorization;
}

public boolean unresolve(
long authorization)
{
final long realmId = authorization & REALM_MASK;
return Long.bitCount(realmId) <= 1 && realmsIdsByName.entrySet().removeIf(e -> e.getValue().realmId == realmId);
}

public JsonWebKey supplyKey(
Expand Down Expand Up @@ -167,4 +188,63 @@ 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 final long realmId;
private final String realmName;

private long nextScopeBit;

private OAuthRealm(
String realmName)
{
assert nextRealmBit < MAX_REALMS;
this.realmName = realmName;
this.realmId = 1L << nextRealmBit++ << MAX_SCOPES;
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved

private long resolve(
String[] scopeNames)
{
long authorization = NO_AUTHORIZATION;
if(nextScopeBit + scopeNames.length < MAX_SCOPES)
{
authorization = realmId;
for (int i = 0; i < scopeNames.length; i++)
{
authorization |= scopeBitsByName.computeIfAbsent(scopeNames[i], this::assignScopeBit);
}
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}
return authorization;
dknoma marked this conversation as resolved.
Show resolved Hide resolved
}

private long lookup(
String[] scopeNames)
{
long authorization = realmId;
for (int i = 0; i < scopeNames.length; i++)
{
authorization |= scopeBitsByName.getOrDefault(scopeNames[i], 0L);
}
return authorization;
}

private long assignScopeBit(
String scopeName)
{
assert nextScopeBit < MAX_SCOPES;
return 1L << nextScopeBit++;
}

@Override
public String toString()
{
return String.format("Realm name: %s\n\tRealm id: %s\n\tScope bits: %s", realmName, realmId, scopeBitsByName);
}
}
}
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 @@ -26,6 +26,7 @@
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;
Expand All @@ -36,7 +37,7 @@
public class OAuthProxyFactoryBuilder implements StreamFactoryBuilder
{
private final Function<String, JsonWebKey> supplyKey;
private final ToLongFunction<String> resolveRealm;
private final ToLongFunction<JsonWebSignature> resolveRealm;
private final Long2ObjectHashMap<OAuthProxy> correlations;

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

public OAuthProxyFactoryBuilder(
Function<String, JsonWebKey> supplyKey,
ToLongFunction<String> resolveRealm)
ToLongFunction<JsonWebSignature> resolveRealm)
{
this.supplyKey = supplyKey;
this.resolveRealm = resolveRealm;
Expand Down
Loading