Skip to content

Commit

Permalink
Merge pull request apache#1335 from afs/bearer-auth2
Browse files Browse the repository at this point in the history
apacheGH-1292: Support for bearer auth
  • Loading branch information
rvesse authored May 27, 2022
2 parents 14c97d7 + fc11305 commit afc165a
Show file tree
Hide file tree
Showing 24 changed files with 1,093 additions and 217 deletions.
19 changes: 13 additions & 6 deletions jena-arq/src/main/java/org/apache/jena/http/HttpLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ public static String basicAuth(String username, String password) {
return "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
}

/**
* Calculate bearer auth header value.
* The token supplied is expected to already be in base 64.
* Use with header "Authorization" (constant {@link HttpNames#hAuthorization}).
*/
public static String bearerAuth(String tokenBase64) {
Objects.requireNonNull(tokenBase64);
if ( tokenBase64.indexOf(' ') >= 0 )
throw new IllegalArgumentException("Base64 token contains a space");
return "Bearer " + tokenBase64;
}

/**
* Get the InputStream from an HttpResponse, handling possible compression settings.
* The application must consume or close the {@code InputStream} (see {@link #finish(InputStream)}).
Expand Down Expand Up @@ -308,14 +320,9 @@ public static URI toRequestURI(String uriStr) {

// Terminology:
// RFC 2616: Request-Line = Method SP Request-URI SP HTTP-Version CRLF

// RFC 7320: request-line = method SP request-target SP HTTP-version CRLF
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.1

// request-target:
// https://datatracker.ietf.org/doc/html/rfc7230#section-5.3
// When it is for the origin server ==> absolute-path [ "?" query ]

// EndpointURI: URL for a service, no query string.

/** Test whether a URI is a service endpoint. It must be absolute, with host and path, and without query string or fragment. */
Expand All @@ -328,7 +335,7 @@ public static boolean isEndpoint(URI uri) {
}

/**
* Return a string (assumed to be a URI) without query string or fragment.
* Return a string (assumed to be an absolute URI) without query string or fragment.
*/
public static String endpoint(String uriStr) {
int idx1 = uriStr.indexOf('?');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class AuthChallenge {
* are lower case and never clash with this name).
*/
public final AuthScheme authScheme;
public final AuthHeader authHeader;
public final String realm;
public final String nonce;
public final String opaque;
Expand All @@ -39,9 +40,9 @@ public class AuthChallenge {

/** Parse "WWW-Authenticate:" challenge message */
static public AuthChallenge parse(String authHeaderStr) {
AuthHeaderParser auth;
AuthHeader auth;
try {
auth = AuthHeaderParser.parse(authHeaderStr);
auth = AuthHeader.parse(authHeaderStr);
if ( auth == null )
return null;
if ( auth.getAuthScheme() == null )
Expand Down Expand Up @@ -71,6 +72,7 @@ static public AuthChallenge parse(String authHeaderStr) {
}

return new AuthChallenge(authScheme,
auth,
get(auth, AuthHttp.strRealm),
get(auth, AuthHttp.strNonce), // Required for digest, not for basic.
get(auth, AuthHttp.strOpaque),
Expand All @@ -81,8 +83,11 @@ static public AuthChallenge parse(String authHeaderStr) {
}
}

private AuthChallenge(AuthScheme authScheme, String realm, String nonce, String opaque, String qop, Map<String, String> authParams) {
private AuthChallenge(AuthScheme authScheme, AuthHeader authHeader, String realm, String nonce, String opaque, String qop, Map<String, String> authParams) {
Objects.requireNonNull(authScheme);
Objects.requireNonNull(authHeader);
this.authScheme = authScheme;
this.authHeader = authHeader;
this.realm = realm;
this.nonce = nonce;
this.opaque = opaque;
Expand All @@ -96,14 +101,20 @@ public String getRealm() {
return authParams.get(AuthHttp.strRealm);
}

private static String get(AuthHeaderParser auth, String s) {
public String getToken() {
if ( ! Objects.equals(authHeader.getAuthScheme(), AuthScheme.BEARER) )
return null;
return authHeader.getBearerToken();
}

private static String get(AuthHeader auth, String s) {
Map<String, String> map = auth.getAuthParams();
if ( map == null )
return null;
return map.get(s);
}

private static String nonNull(AuthHeaderParser auth, String s) {
private static String nonNull(AuthHeader auth, String s) {
Map<String, String> map = auth.getAuthParams();
if ( map == null )
throw new NullPointerException("No auth params");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ public AuthCredentials() {}

public void put(AuthDomain location, PasswordRecord pwRecord) {
// Checks.
URI uri = location.uri;
URI uri = location.getURI();
if ( uri.getRawQuery() != null || uri.getRawFragment() != null )
throw new HttpException("Endpoint URI must not have query string or fragment: "+uri);
authRegistry.put(location, pwRecord);
prefixes.add(uri.toString(), location);
}

public boolean contains(AuthDomain location) {
return prefixes.contains(location.uri.toString());
return prefixes.contains(location.getURI().toString());
}

public List<AuthDomain> registered() {
Expand All @@ -58,9 +58,9 @@ public PasswordRecord get(AuthDomain location) {
if ( pwRecord != null )
return pwRecord;

prefixes.partialSearch(location.uri.toString());
prefixes.partialSearch(location.getURI().toString());

AuthDomain match = prefixes.longestMatch(location.uri.toString());
AuthDomain match = prefixes.longestMatch(location.getURI().toString());
if ( match == null )
return null;
if ( match.getRealm() != null ) {
Expand All @@ -71,7 +71,7 @@ public PasswordRecord get(AuthDomain location) {
}

public void remove(AuthDomain location) {
prefixes.remove(location.uri.toString());
prefixes.remove(location.getURI().toString());
authRegistry.remove(location);
}

Expand Down
11 changes: 7 additions & 4 deletions jena-arq/src/main/java/org/apache/jena/http/auth/AuthDomain.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@
import java.net.URI;
import java.util.Objects;

/** URI and optional realm, as a value-equality pair. */
/** URI, and optional realm, as a value-equality pair. */
public class AuthDomain {
URI uri;
// May be null;
private URI uri;
private String realm;
public static final String noRealm = "";

public AuthDomain(URI uri) {
this(uri, null);
}

public AuthDomain(URI uri, String realm) {
private AuthDomain(URI uri, String realm) {
Objects.requireNonNull(uri);
this.uri = uri;
if ( realm == null )
realm = noRealm;
this.realm = realm;
}

Expand Down
67 changes: 62 additions & 5 deletions jena-arq/src/main/java/org/apache/jena/http/auth/AuthEnv.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;

import org.apache.jena.http.HttpLib;
import org.apache.jena.riot.web.HttpNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -32,6 +34,8 @@ public class AuthEnv {
private AuthCredentials passwordRegistry = new AuthCredentials();
// Challenge setups that are active.
private Map<String, AuthRequestModifier> authModifiers = new ConcurrentHashMap<>();
// Token fetcher for bearer authentication
private BiFunction<URI, AuthChallenge, String> tokenSupplier = null;

private static AuthEnv singleton = new AuthEnv();
public static AuthEnv get() { return singleton; }
Expand All @@ -40,19 +44,19 @@ private AuthEnv() { }

/** Register (username, password) information for a URI endpoint. */
public void registerUsernamePassword(URI uri, String user, String password) {
AuthDomain domain = new AuthDomain(uri, null);
AuthDomain domain = new AuthDomain(uri);
passwordRegistry.put(domain, new PasswordRecord(user, password));
}

/** Check whether there is a registration. */
public boolean hasRegistation(URI uri) {
AuthDomain location = new AuthDomain(uri, null);
AuthDomain location = new AuthDomain(uri);
return passwordRegistry.contains(location);
}

/** Register (username, password) information for a URI endpoint. */
public void unregisterUsernamePassword(URI uri) {
AuthDomain location = new AuthDomain(uri, null);
AuthDomain location = new AuthDomain(uri);
passwordRegistry.remove(location);
// and remove any active modifiers.
authModifiers.remove(uri.toString());
Expand All @@ -65,7 +69,7 @@ public void unregisterUsernamePassword(URI uri) {
* longest prefix entry is returned.
*/
public PasswordRecord getUsernamePassword(URI uri) {
AuthDomain domain = new AuthDomain(uri, null);
AuthDomain domain = new AuthDomain(uri);
return passwordRegistry.get(domain);
}

Expand Down Expand Up @@ -101,13 +105,66 @@ public void registerBasicAuthModifier(String url, String user, String password)
authModifiers.put(serviceEndpoint, basicAuthModifier);
}

void registerAuthModifier(String requestTarget, AuthRequestModifier authModifier) {
/** Register an AuthRequestModifier for a specific request target */
public void registerAuthModifier(String requestTarget, AuthRequestModifier authModifier) {
// Without query string or fragment.
String serviceEndpoint = HttpLib.endpoint(requestTarget);
//AuthEnv.LOG.info("Setup authentication for "+serviceEndpoint);
authModifiers.put(serviceEndpoint, authModifier);
}

/** Remove any AuthRequestModifier for a specific request target */
public void unregisterAuthModifier(String requestTarget) {
String endpointURL = HttpLib.endpoint(requestTarget);
AuthRequestModifier oldMod = authModifiers.remove(endpointURL);
}

/**
* Set the creator of tokens for bearer authentication. The function must return
* null (reject a 401 challenge) or a valid token including encoding (e.g.
* Base64). It must not contain spaces. Requests will fail when the token becomes
* out-of-date and the application will need to set a new token.
* <p>
* The string supplied will be used as-is with no further processing. Supply a
* null argument to clear any previous token supplier.
*/
public void setBearerTokenProvider(BiFunction<URI, AuthChallenge, String> tokenSupplier) {
this.tokenSupplier = tokenSupplier;
}

/**
* Set the tokens for bearer authentication at an specific endpoint. This is
* added to all requests sent to this same request target. Requests will fail
* when the token becomes out-of-date and the application will need to set a new
* token.
* <p>
* The string supplied will be used as-is with no further processing. Supply a
* null argument to clear any previous token supplier.
*/
public void setBearerToken(String requestTarget, String token) {
if ( token == null ) {
unregisterAuthModifier(requestTarget);
return;
}
String endpointURL = HttpLib.endpoint(requestTarget);
AuthRequestModifier authModifier = builder->builder.setHeader(HttpNames.hAuthorization, "Bearer "+token);
registerAuthModifier(requestTarget, authModifier);
}

/**
* Return a bearer auth token to use when responding to a 401 challenge.
* The token must be in the form required for the "Authorization" header,
* including encoding (e.g. Base64). The string supplied is used as-is
* with no further processing.
* <p>
* Return null for "no token" in which case a 401 response is passed back to the
* application.
*/
public String getBearerToken(URI uri, AuthChallenge aHeader) {
if ( tokenSupplier == null )
return null;
return tokenSupplier.apply(uri, aHeader);
}

// Development - do not provide in production systems.
// public void state() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@

package org.apache.jena.http.auth;

public class AuthStringException extends RuntimeException {
public AuthStringException() {
import org.apache.jena.shared.JenaException;

public class AuthException extends JenaException {
public AuthException() {
super();
}

public AuthStringException(String msg) {
public AuthException(String msg) {
super(msg);
}
}
Loading

0 comments on commit afc165a

Please sign in to comment.