Implemented OpenID support for auth_time, nonce and max_age.
Change-Id: I509554ff19a9f5baf6c1add5c6b5c0a07ec76380
diff --git a/core/src/main/java/de/ids_mannheim/korap/config/Attributes.java b/core/src/main/java/de/ids_mannheim/korap/config/Attributes.java
index 2149fef..51e4dab 100644
--- a/core/src/main/java/de/ids_mannheim/korap/config/Attributes.java
+++ b/core/src/main/java/de/ids_mannheim/korap/config/Attributes.java
@@ -2,6 +2,11 @@
public class Attributes {
+ // EM: openid auth_time
+ public static final String AUTHENTICATION_TIME = "auth_time";
+ public static final String DEFAULT_TIME_ZONE = "Europe/Berlin";
+ // -- EM
+
public static final String AUTHORIZATION = "Authorization";
// moved to de.ids_mannheim.korap.config.AuthenticationScheme
// public static final String SESSION_AUTHENTICATION = "session_token";
diff --git a/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java b/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
index 655a98a..5bd0a8d 100644
--- a/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
+++ b/core/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
@@ -133,6 +133,7 @@
public static final int ID_TOKEN_CLAIM_ERROR = 1812;
public static final int ID_TOKEN_SIGNING_FAILED = 1813;
+ public static final int USER_REAUTHENTICATION_REQUIRED = 1814;
/**
diff --git a/full/Changes b/full/Changes
index 3af7de8..dc048a4 100644
--- a/full/Changes
+++ b/full/Changes
@@ -8,8 +8,10 @@
- added state to OAuth2 authorization error response (margaretha)
- implemented OpenID token service for authorization code flow (margaretha)
- implemented signed OpenID token with default algorithm RSA256 (margaretha)
- - added JSON Web Key (JWK) set web-controller listing kustvakt public keys (margaretha)
+ - implemented JSON Web Key (JWK) set web-controller listing kustvakt public keys (margaretha)
- implemented OpenId configuration (margaretha)
+ - added authentication time and support for auth_time in id_token (margaretha)
+ - implemented support for nonce and max_age parameters in OpenID authentication (margaretha)
version 0.60.3
06/06/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
index 2a3b676..e272a34 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
@@ -1,5 +1,7 @@
package de.ids_mannheim.korap.authentication;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
@@ -19,14 +21,15 @@
import de.ids_mannheim.korap.utils.StringUtils;
import de.ids_mannheim.korap.utils.TimeUtils;
-/**
- * Implementation of encoding and decoding access token is moved to
- * {@link TransferEncoding}. Moreover, implementation of HTTP
- * Authentication framework, i.e. creation of authorization header,
- * is defined in {@link HttpAuthorizationHandler}.
+/**
+ * Implementation of encoding and decoding access token is moved to
+ * {@link TransferEncoding}. Moreover, implementation of HTTP
+ * Authentication framework, i.e. creation of authorization header,
+ * is defined in {@link HttpAuthorizationHandler}.
*
- * Basic authentication is intended to be used with a database. It is
- * currently only used for testing using a dummy DAO (@see {@link UserDao})
+ * Basic authentication is intended to be used with a database. It is
+ * currently only used for testing using a dummy DAO (@see
+ * {@link UserDao})
* without passwords.
*
* <br /><br />
@@ -49,8 +52,8 @@
private TransferEncoding transferEncoding;
@Autowired
private FullConfiguration config;
-// @Autowired
-// private EncryptionIface crypto;
+ // @Autowired
+ // private EncryptionIface crypto;
@Autowired
private UserDao dao;
@@ -59,10 +62,13 @@
throws KustvaktException {
String[] values = transferEncoding.decodeBase64(authToken);
User user = dao.getAccount(values[0]);
+ ZonedDateTime authenticationTime =
+ ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
if (user != null) {
TokenContext c = new TokenContext();
c.setUsername(values[0]);
+ c.setAuthenticationTime(authenticationTime);
c.setExpirationTime(TimeUtils.plusSeconds(this.config.getTokenTTL())
.getMillis());
c.setTokenType(getTokenType());
@@ -70,7 +76,8 @@
c.setSecureRequired(false);
// EM: is this secure?
c.setToken(StringUtils.stripTokenType(authToken));
- // fixme: you can make queries, but user sensitive data is off limits?!
+ // fixme: you can make queries, but user sensitive data is
+ // off limits?!
c.addContextParameter(Attributes.SCOPES,
Scopes.Scope.search.toString());
return c;
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
index 566d64b..f490de0 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
@@ -48,6 +48,7 @@
c.setToken(authToken);
c.setTokenType(TokenType.BEARER);
c.addContextParameter(Attributes.SCOPES, scopes);
+ c.setAuthenticationTime(accessToken.getUserAuthenticationTime());
return c;
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java b/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
index 659ce4a..4f624cf 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
@@ -18,6 +18,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -76,6 +77,8 @@
csBuilder.audience((String) attr.get(Attributes.CLIENT_ID));
}
csBuilder.expirationTime(TimeUtils.getNow().plusSeconds(ttl).toDate());
+ csBuilder.claim(Attributes.AUTHENTICATION_TIME,
+ attr.get(Attributes.AUTHENTICATION_TIME));
SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
csBuilder.build());
try {
@@ -183,6 +186,8 @@
signedJWT.getJWTClaimsSet().getAudience().get(0));
c.setExpirationTime(
signedJWT.getJWTClaimsSet().getExpirationTime().getTime());
+ c.setAuthenticationTime((ZonedDateTime) signedJWT.getJWTClaimsSet()
+ .getClaim(Attributes.AUTHENTICATION_TIME));
c.setToken(idtoken);
c.addParams(signedJWT.getJWTClaimsSet().getClaims());
return c;
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
index dcc7499..f7ee4ff 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
@@ -1,5 +1,6 @@
package de.ids_mannheim.korap.oauth2.dao;
+import java.time.ZonedDateTime;
import java.util.Set;
import javax.persistence.EntityManager;
@@ -38,16 +39,22 @@
accessToken.setUserId(authorization.getUserId());
accessToken.setToken(token);
accessToken.setScopes(authorization.getScopes());
+ accessToken.setUserAuthenticationTime(
+ authorization.getUserAuthenticationTime());
entityManager.persist(accessToken);
}
public void storeAccessToken (String token, Set<AccessScope> scopes,
- String userId) throws KustvaktException {
+ String userId, ZonedDateTime authenticationTime)
+ throws KustvaktException {
ParameterChecker.checkObjectValue(scopes, "scopes");
+ ParameterChecker.checkObjectValue(authenticationTime,
+ "authentication time");
AccessToken accessToken = new AccessToken();
accessToken.setToken(token);
accessToken.setScopes(scopes);
accessToken.setUserId(userId);
+ accessToken.setUserAuthenticationTime(authenticationTime);
entityManager.persist(accessToken);
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
index 6bcb11c..82557c0 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
@@ -1,5 +1,6 @@
package de.ids_mannheim.korap.oauth2.dao;
+import java.time.ZonedDateTime;
import java.util.Set;
import javax.persistence.EntityManager;
@@ -29,19 +30,23 @@
private EntityManager entityManager;
public Authorization storeAuthorizationCode (String clientId, String userId,
- String code, Set<AccessScope> scopes, String redirectURI)
- throws KustvaktException {
+ String code, Set<AccessScope> scopes, String redirectURI,
+ ZonedDateTime authenticationTime, String nonce) throws KustvaktException {
ParameterChecker.checkStringValue(clientId, "client_id");
ParameterChecker.checkStringValue(userId, "userId");
ParameterChecker.checkStringValue(code, "authorization code");
ParameterChecker.checkCollection(scopes, "scopes");
-
+ ParameterChecker.checkObjectValue(authenticationTime,
+ "user authentication time");
+
Authorization authCode = new Authorization();
authCode.setCode(code);
authCode.setClientId(clientId);
authCode.setUserId(userId);
authCode.setScopes(scopes);
authCode.setRedirectURI(redirectURI);
+ authCode.setUserAuthenticationTime(authenticationTime);
+ authCode.setNonce(nonce);
entityManager.persist(authCode);
// what if unique fails
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
index f99ac4a..55d0950 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
@@ -37,6 +37,8 @@
private boolean isRevoked;
@Column(name = "total_attempts")
private int totalAttempts;
+ @Column(name = "user_auth_time", updatable = false)
+ private ZonedDateTime userAuthenticationTime;
@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(name="authorization_id")
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/Authorization.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/Authorization.java
index 207512f..bbd954b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/Authorization.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/Authorization.java
@@ -33,12 +33,16 @@
private String userId;
@Column(name = "redirect_uri")
private String redirectURI;
- @Column(name = "created_date", updatable=false)
+ @Column(name = "created_date", updatable = false)
private ZonedDateTime createdDate;
@Column(name = "is_revoked")
private boolean isRevoked;
@Column(name = "total_attempts")
private int totalAttempts;
+ @Column(name = "user_auth_time", updatable = false)
+ private ZonedDateTime userAuthenticationTime;
+ @Column(updatable = false)
+ private String nonce;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "oauth2_authorization_scope",
@@ -49,7 +53,7 @@
uniqueConstraints = @UniqueConstraint(
columnNames = { "authorization_id", "scope_id" }))
private Set<AccessScope> scopes;
-
+
@Override
public String toString () {
return "code: " + code + ", " + "clientId: " + clientId + ", "
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java
index 77f8e17..f1bb28c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java
@@ -2,6 +2,7 @@
import java.net.URI;
import java.net.URISyntaxException;
+import java.time.ZonedDateTime;
import javax.servlet.http.HttpServletRequest;
@@ -40,6 +41,7 @@
* @param request
* @param authzRequest
* @param username
+ * @param authTime
* @return redirect URI containing authorization code if
* successful.
*
@@ -47,7 +49,8 @@
* @throws OAuthSystemException
*/
public String requestAuthorizationCode (HttpServletRequest request,
- OAuthAuthzRequest authzRequest, String username)
+ OAuthAuthzRequest authzRequest, String username,
+ ZonedDateTime authenticationTime)
throws OAuthSystemException, KustvaktException {
String code = oauthIssuer.authorizationCode();
@@ -71,7 +74,8 @@
String scope;
try {
scope = createAuthorization(username, authzRequest.getClientId(),
- redirectUri, authzRequest.getScopes(), code);
+ redirectUri, authzRequest.getScopes(), code,
+ authenticationTime, null);
}
catch (KustvaktException e) {
e.setRedirectUri(redirectURI);
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
index 0df9a3c..913db9f 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
@@ -1,5 +1,6 @@
package de.ids_mannheim.korap.oauth2.oltu.service;
+import java.time.ZonedDateTime;
import java.util.Set;
import javax.ws.rs.core.Response.Status;
@@ -47,17 +48,24 @@
return createsAccessTokenResponse(authorization);
}
else if (grantType.equals(GrantType.PASSWORD.toString())) {
- requestAccessTokenWithPassword(oAuthRequest.getUsername(),
- oAuthRequest.getPassword(), oAuthRequest.getScopes(),
- oAuthRequest.getClientId(), oAuthRequest.getClientSecret());
+ ZonedDateTime authenticationTime = requestAccessTokenWithPassword(
+ oAuthRequest.getUsername(), oAuthRequest.getPassword(),
+ oAuthRequest.getScopes(), oAuthRequest.getClientId(),
+ oAuthRequest.getClientSecret());
return createsAccessTokenResponse(oAuthRequest.getScopes(),
- oAuthRequest.getUsername());
+ oAuthRequest.getUsername(), authenticationTime);
}
else if (grantType.equals(GrantType.CLIENT_CREDENTIALS.toString())) {
- Set<String> scopes = requestAccessTokenWithClientCredentials(
- oAuthRequest.getClientId(), oAuthRequest.getClientSecret(),
- oAuthRequest.getScopes());
- return createsAccessTokenResponse(scopes, null);
+ ZonedDateTime authenticationTime =
+ requestAccessTokenWithClientCredentials(
+ oAuthRequest.getClientId(),
+ oAuthRequest.getClientSecret(),
+ oAuthRequest.getScopes());
+
+ Set<String> scopes =
+ scopeService.filterScopes(oAuthRequest.getScopes(),
+ config.getClientCredentialsScopes());
+ return createsAccessTokenResponse(scopes, null, authenticationTime);
}
else {
throw new KustvaktException(StatusCodes.UNSUPPORTED_GRANT_TYPE,
@@ -71,19 +79,23 @@
* Creates an OAuthResponse containing an access token and a
* refresh token with type Bearer.
*
+ * @param authenticationTime
+ *
* @return an OAuthResponse containing an access token
* @throws OAuthSystemException
* @throws KustvaktException
*/
private OAuthResponse createsAccessTokenResponse (Set<String> scopes,
- String userId) throws OAuthSystemException, KustvaktException {
+ String userId, ZonedDateTime authenticationTime)
+ throws OAuthSystemException, KustvaktException {
String accessToken = oauthIssuer.accessToken();
// String refreshToken = oauthIssuer.refreshToken();
Set<AccessScope> accessScopes =
scopeService.convertToAccessScope(scopes);
- tokenDao.storeAccessToken(accessToken, accessScopes, userId);
+ tokenDao.storeAccessToken(accessToken, accessScopes, userId,
+ authenticationTime);
return OAuthASResponse.tokenResponse(Status.OK.getStatusCode())
.setAccessToken(accessToken)
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdAuthorizationService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdAuthorizationService.java
index d6f719c..3e757f3 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdAuthorizationService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdAuthorizationService.java
@@ -2,6 +2,8 @@
import java.net.URI;
import java.net.URISyntaxException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -20,7 +22,9 @@
import com.nimbusds.oauth2.sdk.id.State;
import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import com.nimbusds.openid.connect.sdk.Nonce;
+import de.ids_mannheim.korap.config.Attributes;
import de.ids_mannheim.korap.exceptions.KustvaktException;
import de.ids_mannheim.korap.exceptions.StatusCodes;
import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
@@ -53,7 +57,8 @@
}
public URI requestAuthorizationCode (Map<String, String> map,
- String username, boolean isAuthentication)
+ String username, boolean isAuthentication,
+ ZonedDateTime authenticationTime)
throws KustvaktException, ParseException {
AuthorizationCode code = new AuthorizationCode();
@@ -61,15 +66,15 @@
if (isAuthentication) {
AuthenticationRequest authRequest = null;
authRequest = AuthenticationRequest.parse(map);
- redirectUri =
- handleAuthenticationRequest(authRequest, code, username);
+ redirectUri = handleAuthenticationRequest(authRequest, code,
+ username, authenticationTime);
return new AuthenticationSuccessResponse(redirectUri, code, null,
null, authRequest.getState(), null, null).toURI();
}
else {
AuthorizationRequest authzRequest = AuthorizationRequest.parse(map);
- redirectUri =
- handleAuthorizationRequest(authzRequest, code, username);
+ redirectUri = handleAuthorizationRequest(authzRequest, code,
+ username, authenticationTime, null);
return new AuthorizationSuccessResponse(redirectUri, code, null,
authzRequest.getState(), null).toURI();
@@ -77,7 +82,9 @@
}
private URI handleAuthorizationRequest (AuthorizationRequest authzRequest,
- AuthorizationCode code, String username) throws KustvaktException {
+ AuthorizationCode code, String username,
+ ZonedDateTime authenticationTime, String nonce)
+ throws KustvaktException {
URI redirectUri = authzRequest.getRedirectionURI();
String redirectUriStr =
@@ -102,9 +109,8 @@
Scope scope = authzRequest.getScope();
Set<String> scopeSet = (scope != null)
? new HashSet<>(scope.toStringList()) : null;
-
createAuthorization(username, clientId, redirectUriStr, scopeSet,
- code.getValue());
+ code.getValue(), authenticationTime, nonce);
}
catch (KustvaktException e) {
e.setRedirectUri(redirectUri);
@@ -115,12 +121,89 @@
}
+ /**
+ * Kustvakt does not support the following parameters:
+ * <em>claims</em>, <em>requestURI</em>, <em>requestObject</em>,
+ * <em>id_token_hint</em>, and ignores them if they are included
+ * in an authentication request. Kustvakt provides minimum support
+ * for <em>acr_values</em> by not throwing an error when it is
+ * included in an authentication request.
+ *
+ * <p>Parameters related to user interface are also ignored,
+ * namely <em>display</em>, <em>prompt</em>,
+ * <em>ui_locales</em>, <em>login_hint</em>. However,
+ * <em>display</em>, <em>prompt</em>, and <em>ui_locales</em>
+ * must be supported by Kalamar. The minimum level of
+ * support required for these parameters is simply that its use
+ * must not result in an error.</p>
+ *
+ * <p>Some Authentication request parameters in addition to
+ * OAuth2.0 authorization parameters according to OpenID connect
+ * core 1.0 Specification:</p>
+ *
+ * <ul>
+ *
+ * <li>nonce</li>
+ * <p> OPTIONAL. The value is passed through unmodified from the
+ * Authentication Request to the ID Token.</p>
+ *
+ * <li>max_age</li>
+ * <p>OPTIONAL. Maximum Authentication Age in seconds. If the
+ * elapsed time is
+ * greater than this value, the OpenID Provider MUST attempt
+ * to actively re-authenticate the End-User. When max_age is used,
+ * the ID Token returned MUST include an auth_time Claim
+ * Value.</p>
+ *
+ * <li>claims</li>
+ * <p>Support for the claims parameter is OPTIONAL. Should an OP
+ * (openid provider) not support this parameter and an RP (relying
+ * party /client) uses it, the OP SHOULD return a set of Claims to
+ * the RP that it believes would be useful to the RP and the
+ * End-User using whatever heuristics it believes are
+ * appropriate.</p>
+ *
+ * </ul>
+ *
+ * @see "OpenID Connect Core 1.0 specification"
+ *
+ * @param authRequest
+ * @param code
+ * @param username
+ * @param authenticationTime
+ * @return
+ * @throws KustvaktException
+ */
private URI handleAuthenticationRequest (AuthenticationRequest authRequest,
- AuthorizationCode code, String username) throws KustvaktException {
+ AuthorizationCode code, String username,
+ ZonedDateTime authenticationTime) throws KustvaktException {
// TO DO: extra checking for authentication params?
+ Nonce nonce = authRequest.getNonce();
+ String nonceValue = null;
+ if (nonce != null && !nonce.getValue().isEmpty()) {
+ nonceValue = nonce.getValue();
+ }
+
+ checkMaxAge(authRequest.getMaxAge(), authenticationTime);
+
AuthorizationRequest request = authRequest;
- return handleAuthorizationRequest(request, code, username);
+ return handleAuthorizationRequest(request, code, username,
+ authenticationTime, nonceValue);
+ }
+
+ private void checkMaxAge (int maxAge, ZonedDateTime authenticationTime) throws KustvaktException {
+ if (maxAge > 0) {
+ ZonedDateTime now =
+ ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+
+ if (authenticationTime.plusSeconds(maxAge).isBefore(now)) {
+ throw new KustvaktException(
+ StatusCodes.USER_REAUTHENTICATION_REQUIRED,
+ "User reauthentication is required because the authentication "
+ + "time is too old according to max_age");
+ }
+ }
}
@Override
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
index cedf59b..0d943c0 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
@@ -2,6 +2,7 @@
import java.net.URI;
import java.security.PrivateKey;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.Set;
@@ -33,6 +34,7 @@
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.oauth2.sdk.token.Tokens;
+import com.nimbusds.openid.connect.sdk.Nonce;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
@@ -114,7 +116,9 @@
if (scope.contains("openid")) {
JWTClaimsSet claims = createIdTokenClaims(
- authorization.getClientId(), authorization.getUserId());
+ authorization.getClientId(), authorization.getUserId(),
+ authorization.getUserAuthenticationTime(),
+ authorization.getNonce());
SignedJWT idToken = signIdToken(claims,
// default
new JWSHeader(JWSAlgorithm.RS256),
@@ -157,7 +161,8 @@
return new String[] { clientId, clientSecret };
}
- private JWTClaimsSet createIdTokenClaims (String client_id, String username)
+ private JWTClaimsSet createIdTokenClaims (String client_id, String username,
+ ZonedDateTime authenticationTime, String nonce)
throws KustvaktException {
// A locally unique and never reassigned identifier within the
// Issuer for the End-User
@@ -172,6 +177,13 @@
IDTokenClaimsSet claims =
new IDTokenClaimsSet(iss, sub, audList, exp, iat);
+
+ Date authTime = Date.from(authenticationTime.toInstant());
+ claims.setAuthenticationTime(authTime);
+ if (nonce != null && !nonce.isEmpty()) {
+ claims.setNonce(new Nonce(nonce));
+ }
+
try {
return claims.toJWTClaimsSet();
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
index f1131b0..638415b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
@@ -43,12 +43,15 @@
* @param redirectUri
* @param scopeSet
* @param code
+ * @param authenticationTime
+ * user authentication time
+ * @param nonce
* @return
* @throws KustvaktException
*/
public String createAuthorization (String username, String clientId,
- String redirectUri, Set<String> scopeSet, String code)
- throws KustvaktException {
+ String redirectUri, Set<String> scopeSet, String code,
+ ZonedDateTime authenticationTime, String nonce) throws KustvaktException {
if (scopeSet == null || scopeSet.isEmpty()) {
scopeSet = config.getDefaultAccessScopes();
@@ -56,7 +59,7 @@
Set<AccessScope> scopes = scopeService.convertToAccessScope(scopeSet);
authorizationDao.storeAuthorizationCode(clientId, username, code,
- scopes, redirectUri);
+ scopes, redirectUri, authenticationTime, nonce);
return String.join(" ", scopeSet);
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
index c64274d..fd251be 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
@@ -1,5 +1,7 @@
package de.ids_mannheim.korap.oauth2.service;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -101,12 +103,11 @@
* @param clientSecret
* client_secret, required if client_secret was issued
* for the client in client registration.
- * @return an OAuthResponse containing an access token if
- * successful
+ * @return authentication time
* @throws KustvaktException
* @throws OAuthSystemException
*/
- protected void requestAccessTokenWithPassword (String username,
+ protected ZonedDateTime requestAccessTokenWithPassword (String username,
String password, Set<String> scopes, String clientId,
String clientSecret) throws KustvaktException {
@@ -118,11 +119,11 @@
OAuth2Error.UNAUTHORIZED_CLIENT);
}
- authenticateUser(username, password, scopes);
+ return authenticateUser(username, password, scopes);
// verify or limit scopes ?
}
- public void authenticateUser (String username, String password,
+ public ZonedDateTime authenticateUser (String username, String password,
Set<String> scopes) throws KustvaktException {
if (username == null || username.isEmpty()) {
throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
@@ -140,6 +141,10 @@
authenticationManager.authenticate(
config.getOAuth2passwordAuthentication(), username, password,
attributes);
+
+ ZonedDateTime authenticationTime =
+ ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+ return authenticationTime;
}
/**
@@ -151,12 +156,11 @@
* @param clientSecret
* client_secret parameter, required
* @param scopes
- * @return an OAuthResponse containing an access token if
- * successful
+ * @return authentication time
* @throws KustvaktException
* @throws OAuthSystemException
*/
- protected Set<String> requestAccessTokenWithClientCredentials (
+ protected ZonedDateTime requestAccessTokenWithClientCredentials (
String clientId, String clientSecret, Set<String> scopes)
throws KustvaktException {
@@ -170,17 +174,16 @@
// OAuth2Client client =
clientService.authenticateClient(clientId, clientSecret);
- // if (client.isNative()) {
+ // if (!client.isNative()) {
// throw new KustvaktException(
// StatusCodes.CLIENT_AUTHENTICATION_FAILED,
// "Client credentials grant is not allowed for third party
// clients",
// OAuth2Error.UNAUTHORIZED_CLIENT);
// }
-
- scopes = scopeService.filterScopes(scopes,
- config.getClientCredentialsScopes());
- return scopes;
+ ZonedDateTime authenticationTime =
+ ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+ return authenticationTime;
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java b/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
index becdf9b..851631a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
+++ b/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
@@ -1,6 +1,8 @@
package de.ids_mannheim.korap.security.context;
import java.io.Serializable;
+import java.time.ZonedDateTime;
+import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@@ -10,7 +12,6 @@
import de.ids_mannheim.korap.constant.TokenType;
import de.ids_mannheim.korap.exceptions.KustvaktException;
import de.ids_mannheim.korap.user.User;
-import de.ids_mannheim.korap.user.User.UserFactory;
import de.ids_mannheim.korap.utils.JsonUtils;
import de.ids_mannheim.korap.utils.TimeUtils;
import lombok.AccessLevel;
@@ -19,12 +20,17 @@
import lombok.Setter;
/**
+ * EM:
+ * - change datatype of tokenType from string to enum
+ * - added authenticationTime
+ *
* @author hanl
* @date 27/01/2014
*/
@Data
public class TokenContext implements java.security.Principal, Serializable {
+ private ZonedDateTime authenticationTime;
/**
* session relevant data. Are never persisted into a database
*/
@@ -71,8 +77,9 @@
public boolean match (TokenContext other) {
if (other.getToken().equals(this.token))
if (this.getHostAddress().equals(this.hostAddress))
- // user agent should be irrelvant -- what about os system version?
- // if (other.getUserAgent().equals(this.userAgent))
+ // user agent should be irrelvant -- what about os
+ // system version?
+ // if (other.getUserAgent().equals(this.userAgent))
return true;
return false;
}
@@ -99,7 +106,7 @@
}
- //todo: complete
+ // todo: complete
public static TokenContext fromJSON (String s) throws KustvaktException {
JsonNode node = JsonUtils.readTree(s);
TokenContext c = new TokenContext();
@@ -116,11 +123,10 @@
TokenContext c = new TokenContext();
if (node != null) {
c.setToken(node.path("token").asText());
- c.setTokenType(TokenType.valueOf(
- node.path("token_type").asText()));
+ c.setTokenType(TokenType.valueOf(node.path("token_type").asText()));
c.setExpirationTime(node.path("expires_in").asLong());
- c.addContextParameter("refresh_token", node.path("refresh_token")
- .asText());
+ c.addContextParameter("refresh_token",
+ node.path("refresh_token").asText());
}
return c;
@@ -139,12 +145,12 @@
}
- public String toJson() throws KustvaktException {
+ public String toJson () throws KustvaktException {
return JsonUtils.toJSON(this.statusMap());
}
- public boolean isDemo() {
+ public boolean isDemo () {
return User.UserFactory.isDemo(this.username);
}
@@ -155,4 +161,14 @@
return this.getUsername();
}
+
+ public ZonedDateTime getAuthenticationTime () {
+ return authenticationTime;
+ }
+
+
+ public void setAuthenticationTime (ZonedDateTime authTime) {
+ this.authenticationTime = authTime;
+ }
+
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java b/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
index 0d23598..f7bef6a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
@@ -27,7 +27,8 @@
public WebApplicationException throwit (KustvaktException e) {
Response r;
- if (e.getStatusCode() == StatusCodes.AUTHORIZATION_FAILED
+ if (e.getStatusCode() == StatusCodes.USER_REAUTHENTICATION_REQUIRED
+ || e.getStatusCode() == StatusCodes.AUTHORIZATION_FAILED
|| e.getStatusCode() >= StatusCodes.AUTHENTICATION_FAILED) {
String notification = buildNotification(e.getStatusCode(),
e.getMessage(), e.getEntity());
@@ -38,8 +39,10 @@
.entity(e.getNotification()).build();
}
else {
+ String notification = buildNotification(e.getStatusCode(),
+ e.getMessage(), e.getEntity());
r = Response.status(getStatus(e.getStatusCode()))
- .entity(buildNotification(e)).build();
+ .entity(notification).build();
}
return new WebApplicationException(r);
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java b/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
index eb6a19e..32152de 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
@@ -10,6 +10,7 @@
import javax.ws.rs.core.Response.Status;
import org.apache.http.HttpHeaders;
+import org.apache.http.HttpStatus;
import org.springframework.stereotype.Service;
import com.nimbusds.oauth2.sdk.AccessTokenResponse;
@@ -23,6 +24,7 @@
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
import de.ids_mannheim.korap.interfaces.db.AuditingIface;
import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
import net.minidev.json.JSONObject;
@@ -97,6 +99,12 @@
ErrorObject errorObject = createErrorObject(e);
errorObject = errorObject.setDescription(e.getMessage());
if (redirectURI == null) {
+ // if (e.getStatusCode()
+ // .equals(StatusCodes.USER_REAUTHENTICATION_REQUIRED)) {
+ // return Response.status(HttpStatus.SC_UNAUTHORIZED)
+ // .entity(e.getMessage()).build();
+ // }
+
return Response.status(errorObject.getHTTPStatusCode())
.entity(errorObject.toJSONObject()).build();
}
@@ -121,7 +129,13 @@
ErrorObject errorObject = errorObjectMap.get(errorCode);
if (errorObject == null) {
- errorObject = new ErrorObject(e.getEntity(), e.getMessage());
+ if (errorCode != null && !errorCode.isEmpty()
+ && !errorCode.equals("[]")) {
+ errorObject = new ErrorObject(e.getEntity(), e.getMessage());
+ }
+ else{
+ throw throwit(e);
+ }
}
return errorObject;
}
@@ -167,14 +181,14 @@
String jsonString = tokenResponse.toJSONObject().toJSONString();
return createResponse(status, jsonString);
}
-
+
public Response createResponse (TokenErrorResponse tokenResponse,
Status status) {
String jsonString = tokenResponse.toJSONObject().toJSONString();
return createResponse(status, jsonString);
}
-
- private Response createResponse(Status status, Object entity){
+
+ private Response createResponse (Status status, Object entity) {
ResponseBuilder builder = Response.status(status);
builder.entity(entity);
builder.header(HttpHeaders.CACHE_CONTROL, "no-store");
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
index 94c6a7f..6e7c012 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
@@ -1,5 +1,7 @@
package de.ids_mannheim.korap.web.controller;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Iterator; // 07.02.17/FB
import java.util.List;
@@ -126,41 +128,42 @@
}
// EM: testing using spring security authentication manager
- @GET
- @Path("ldap/token")
- public Response requestToken (@Context HttpHeaders headers,
- @Context Locale locale,
- @HeaderParam(ContainerRequest.USER_AGENT) String agent,
- @HeaderParam(ContainerRequest.HOST) String host,
- @HeaderParam("referer-url") String referer,
- @QueryParam("scope") String scopes,
- // @Context WebServiceContext wsContext, // FB
- @Context SecurityContext securityContext) {
-
- Map<String, Object> attr = new HashMap<>();
- if (scopes != null && !scopes.isEmpty())
- attr.put(Attributes.SCOPES, scopes);
- attr.put(Attributes.HOST, host);
- attr.put(Attributes.USER_AGENT, agent);
-
- User user = new KorAPUser();
- user.setUsername(securityContext.getUserPrincipal().getName());
- controller.setAccessAndLocation(user, headers);
- if (DEBUG_LOG == true) System.out.printf(
- "Debug: /token/: location=%s, access='%s'.\n",
- user.locationtoString(), user.accesstoString());
- attr.put(Attributes.LOCATION, user.getLocation());
- attr.put(Attributes.CORPUS_ACCESS, user.getCorpusAccess());
-
- try {
- TokenContext context = controller.createTokenContext(user, attr,
- TokenType.API);
- return Response.ok(context.toJson()).build();
- }
- catch (KustvaktException e) {
- throw kustvaktResponseHandler.throwit(e);
- }
- }
+// @Deprecated
+// @GET
+// @Path("ldap/token")
+// public Response requestToken (@Context HttpHeaders headers,
+// @Context Locale locale,
+// @HeaderParam(ContainerRequest.USER_AGENT) String agent,
+// @HeaderParam(ContainerRequest.HOST) String host,
+// @HeaderParam("referer-url") String referer,
+// @QueryParam("scope") String scopes,
+// // @Context WebServiceContext wsContext, // FB
+// @Context SecurityContext securityContext) {
+//
+// Map<String, Object> attr = new HashMap<>();
+// if (scopes != null && !scopes.isEmpty())
+// attr.put(Attributes.SCOPES, scopes);
+// attr.put(Attributes.HOST, host);
+// attr.put(Attributes.USER_AGENT, agent);
+//
+// User user = new KorAPUser();
+// user.setUsername(securityContext.getUserPrincipal().getName());
+// controller.setAccessAndLocation(user, headers);
+// if (DEBUG_LOG == true) System.out.printf(
+// "Debug: /token/: location=%s, access='%s'.\n",
+// user.locationtoString(), user.accesstoString());
+// attr.put(Attributes.LOCATION, user.getLocation());
+// attr.put(Attributes.CORPUS_ACCESS, user.getCorpusAccess());
+//
+// try {
+// TokenContext context = controller.createTokenContext(user, attr,
+// TokenType.API);
+// return Response.ok(context.toJson()).build();
+// }
+// catch (KustvaktException e) {
+// throw kustvaktResponseHandler.throwit(e);
+// }
+// }
@GET
@@ -256,6 +259,13 @@
// Userdata data = this.controller.getUserData(user, UserDetails.class); // Implem. by Hanl
// todo: is this necessary?
// attr.putAll(data.fields());
+
+ // EM: add authentication time
+ ZonedDateTime authenticationTime =
+ ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
+ attr.put(Attributes.AUTHENTICATION_TIME, authenticationTime);
+ // -- EM
+
controller.setAccessAndLocation(user, headers);
if (DEBUG_LOG == true) System.out.printf(
"Debug: /apiToken/: location=%s, access='%s'.\n",
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
index 3726f17..4a70807 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
@@ -1,5 +1,7 @@
package de.ids_mannheim.korap.web.controller;
+import java.time.ZonedDateTime;
+
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
@@ -79,6 +81,7 @@
TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
String username = tokenContext.getUsername();
+ ZonedDateTime authTime = tokenContext.getAuthenticationTime();
HttpServletRequest requestWithForm =
new FormRequestWrapper(request, form);
@@ -86,7 +89,7 @@
OAuth2AuthorizationRequest authzRequest =
new OAuth2AuthorizationRequest(requestWithForm);
String uri = authorizationService.requestAuthorizationCode(
- requestWithForm, authzRequest, username);
+ requestWithForm, authzRequest, username, authTime);
return responseHandler.sendRedirect(uri);
}
catch (OAuthSystemException e) {
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
index cf4fe81..a579866 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
@@ -3,6 +3,7 @@
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
+import java.time.ZonedDateTime;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
@@ -55,7 +56,7 @@
private JWKService jwkService;
@Autowired
private OpenIdConfigService configService;
-
+
@Autowired
private OpenIdResponseHandler openIdResponseHandler;
@@ -64,11 +65,12 @@
*
* <ul>
* <li>scope: MUST contain "openid" for OpenID Connect
- * requests,</li>
- * <li>response_type,</li>
- * <li>client_id,</li>
+ * requests</li>
+ * <li>response_type: only "code" is supported</li>
+ * <li>client_id: client identifier given by Kustvakt during
+ * client registration</li>
* <li>redirect_uri: MUST match a pre-registered redirect uri
- * during client registration.</li>
+ * during client registration</li>
* </ul>
*
* Other parameters:
@@ -77,7 +79,7 @@
* <li>state (recommended): Opaque value used to maintain state
* between the request and the callback.</li>
* <li>response_mode (optional) : mechanism to be used for
- * returning parameters</li>
+ * returning parameters, only "query" is supported</li>
* <li>nonce (optional): String value used to associate a Client
* session with an ID Token,
* and to mitigate replay attacks. </li>
@@ -120,6 +122,7 @@
TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
String username = tokenContext.getUsername();
+ ZonedDateTime authTime = tokenContext.getAuthenticationTime();
Map<String, String> map = MapUtils.toMap(form);
State state = authzService.retrieveState(map);
@@ -136,7 +139,7 @@
authzService.checkRedirectUriParam(map);
}
uri = authzService.requestAuthorizationCode(map, username,
- isAuthentication);
+ isAuthentication, authTime);
}
catch (ParseException e) {
return openIdResponseHandler.createAuthorizationErrorResponse(e,
@@ -213,7 +216,8 @@
/**
* When supporting discovery, must be available at
* {issuer_uri}/.well-known/openid-configuration
- * @return
+ *
+ * @return
*
* @return
*/
diff --git a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
index 2a203f3..c330c92 100644
--- a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
+++ b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
@@ -33,9 +33,9 @@
"This is a test native public client.");
-INSERT INTO oauth2_access_token(token,user_id)
-VALUES("249c64a77f40e2b5504982cc5521b596","dory");
+INSERT INTO oauth2_access_token(token,user_id, user_auth_time)
+VALUES("249c64a77f40e2b5504982cc5521b596","dory","2018-05-30 16:24:10");
-INSERT INTO oauth2_access_token(token,user_id,created_date)
-VALUES("fia0123ikBWn931470H8s5gRqx7Moc4p","marlin","2018-05-30 16:25:50")
-
\ No newline at end of file
+INSERT INTO oauth2_access_token(token,user_id,created_date, user_auth_time)
+VALUES("fia0123ikBWn931470H8s5gRqx7Moc4p","marlin","2018-05-30 16:25:50",
+"2018-05-30 16:23:10");
diff --git a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
index fae3fac..fb85417 100644
--- a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
@@ -24,6 +24,8 @@
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_revoked BOOLEAN DEFAULT 0,
total_attempts INTEGER DEFAULT 0,
+ user_auth_time TIMESTAMP NOT NULL,
+ nonce TEXT DEFAULT NULL,
FOREIGN KEY (client_id)
REFERENCES oauth2_client(id),
UNIQUE INDEX authorization_index(code, client_id)
@@ -52,6 +54,7 @@
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_revoked BOOLEAN DEFAULT 0,
total_attempts INTEGER DEFAULT 0,
+ user_auth_time TIMESTAMP NOT NULL,
FOREIGN KEY (authorization_id)
REFERENCES oauth2_authorization(id)
);
diff --git a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
index dc03098..71a2d75 100644
--- a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
@@ -23,9 +23,11 @@
client_id VARCHAR(100) NOT NULL,
user_id VARCHAR(100) NOT NULL,
redirect_uri TEXT DEFAULT NULL,
- created_date timestamp DEFAULT (datetime('now','localtime')),
+ created_date TIMESTAMP DEFAULT (datetime('now','localtime')),
is_revoked BOOLEAN DEFAULT 0,
total_attempts INTEGER DEFAULT 0,
+ user_auth_time TIMESTAMP NOT NULL,
+ nonce TEXT DEFAULT NULL,
FOREIGN KEY (client_id)
REFERENCES oauth2_client(id)
);
@@ -54,9 +56,10 @@
token VARCHAR(255) NOT NULL,
authorization_id INTEGER DEFAULT NULL,
user_id VARCHAR(100) DEFAULT NULL,
- created_date timestamp DEFAULT (datetime('now','localtime')),
+ created_date TIMESTAMP DEFAULT (datetime('now','localtime')),
is_revoked BOOLEAN DEFAULT 0,
total_attempts INTEGER DEFAULT 0,
+ user_auth_time TIMESTAMP NOT NULL,
FOREIGN KEY (authorization_id)
REFERENCES oauth2_authorization(id)
);
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
index 00b1eb6..07344fd 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
@@ -25,18 +25,9 @@
public class OAuth2AccessTokenTest extends SpringJerseyTest {
// test access token for username: dory
- private static String testAccessToken;
-
- @BeforeClass
- public static void init () throws IOException {
- InputStream is = OAuth2AccessTokenTest.class.getClassLoader()
- .getResourceAsStream("test-oauth2.token");
-
- try (BufferedReader reader =
- new BufferedReader(new InputStreamReader(is));) {
- testAccessToken = reader.readLine();
- }
- }
+ // see:
+ // full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
+ private static String testAccessToken = "249c64a77f40e2b5504982cc5521b596";
@Test
public void testListVC () throws KustvaktException {
@@ -84,7 +75,8 @@
JsonNode node = JsonUtils.readTree(ent);
assertEquals(StatusCodes.INVALID_ACCESS_TOKEN,
node.at("/errors/0/0").asInt());
- assertEquals("Access token is not found", node.at("/errors/0/1").asText());
+ assertEquals("Access token is not found",
+ node.at("/errors/0/1").asText());
}
@Test
@@ -97,12 +89,13 @@
.get(ClientResponse.class);
String ent = response.getEntity(String.class);
-
+
assertEquals(ClientResponse.Status.UNAUTHORIZED.getStatusCode(),
response.getStatus());
JsonNode node = JsonUtils.readTree(ent);
assertEquals(StatusCodes.EXPIRED, node.at("/errors/0/0").asInt());
- assertEquals("Access token is expired", node.at("/errors/0/1").asText());
+ assertEquals("Access token is expired",
+ node.at("/errors/0/1").asText());
}
}
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
index 6594fdd..9032c67 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
@@ -13,6 +13,7 @@
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
+import org.apache.http.HttpStatus;
import org.apache.http.entity.ContentType;
import org.apache.oltu.oauth2.common.message.types.TokenType;
import org.junit.Test;
@@ -27,6 +28,7 @@
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
@@ -38,6 +40,7 @@
import de.ids_mannheim.korap.config.FullConfiguration;
import de.ids_mannheim.korap.config.SpringJerseyTest;
import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
import de.ids_mannheim.korap.utils.JsonUtils;
@@ -185,7 +188,6 @@
throws KustvaktException {
ClientResponse response = sendAuthorizationRequest(form);
- System.out.println(response.getEntity(String.class));
URI location = response.getLocation();
assertEquals(MediaType.APPLICATION_FORM_URLENCODED,
response.getType().toString());
@@ -254,21 +256,54 @@
}
@Test
+ public void testRequestAuthorizationCodeAuthenticationTooOld ()
+ throws KustvaktException {
+ MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+ form.add("response_type", "code");
+ form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ form.add("redirect_uri", redirectUri);
+ form.add("scope", "openid");
+ form.add("max_age", "1800");
+
+ ClientResponse response =
+ resource().path("oauth2").path("openid").path("authorize")
+ .header(Attributes.AUTHORIZATION,
+ "Bearer 249c64a77f40e2b5504982cc5521b596")
+ .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+ .header(HttpHeaders.CONTENT_TYPE,
+ ContentType.APPLICATION_FORM_URLENCODED)
+ .entity(form).post(ClientResponse.class);
+
+ assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus());
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(StatusCodes.USER_REAUTHENTICATION_REQUIRED,
+ node.at("/errors/0/0").asInt());
+ assertEquals(
+ "User reauthentication is required because the authentication "
+ + "time is too old according to max_age",
+ node.at("/errors/0/1").asText());
+ }
+
+ @Test
public void testRequestAccessToken ()
throws KustvaktException, ParseException, InvalidKeySpecException,
NoSuchAlgorithmException, JOSEException {
String client_id = "fCBbQkAyYzI4NzUxMg";
+ String nonce = "thisIsMyNonce";
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("response_type", "code");
form.add("client_id", client_id);
form.add("redirect_uri", redirectUri);
form.add("scope", "openid");
form.add("state", "thisIsMyState");
+ form.add("nonce", nonce);
ClientResponse response = sendAuthorizationRequest(form);
URI location = response.getLocation();
MultiValueMap<String, String> params =
UriComponentsBuilder.fromUri(location).build().getQueryParams();
+ assertEquals("thisIsMyState", params.getFirst("state"));
String code = params.getFirst("code");
MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
@@ -280,7 +315,6 @@
ClientResponse tokenResponse = sendTokenRequest(tokenForm);
String entity = tokenResponse.getEntity(String.class);
- // System.out.println(entity);
JsonNode node = JsonUtils.readTree(entity);
assertNotNull(node.at("/access_token").asText());
@@ -291,12 +325,12 @@
String id_token = node.at("/id_token").asText();
assertNotNull(id_token);
- verifyingIdToken(id_token, username, client_id);
+ verifyingIdToken(id_token, username, client_id, nonce);
}
private void verifyingIdToken (String id_token, String username,
- String client_id) throws ParseException, InvalidKeySpecException,
- NoSuchAlgorithmException, JOSEException {
+ String client_id, String nonce) throws ParseException,
+ InvalidKeySpecException, NoSuchAlgorithmException, JOSEException {
JWKSet keySet = config.getPublicKeySet();
RSAKey publicKey = (RSAKey) keySet.getKeyByKeyId(config.getRsaKeyId());
@@ -304,13 +338,13 @@
JWSVerifier verifier = new RSASSAVerifier(publicKey);
assertTrue(signedJWT.verify(verifier));
- assertEquals(client_id,
- signedJWT.getJWTClaimsSet().getAudience().get(0));
- assertEquals(username, signedJWT.getJWTClaimsSet().getSubject());
- assertEquals(config.getIssuerURI().toString(),
- signedJWT.getJWTClaimsSet().getIssuer());
- assertTrue(new Date()
- .before(signedJWT.getJWTClaimsSet().getExpirationTime()));
+ JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
+ assertEquals(client_id, claimsSet.getAudience().get(0));
+ assertEquals(username, claimsSet.getSubject());
+ assertEquals(config.getIssuerURI().toString(), claimsSet.getIssuer());
+ assertTrue(new Date().before(claimsSet.getExpirationTime()));
+ assertNotNull(claimsSet.getClaim(Attributes.AUTHENTICATION_TIME));
+ assertEquals(nonce, claimsSet.getClaim("nonce"));
}
@Test
@@ -319,14 +353,14 @@
.path("jwks").get(ClientResponse.class);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
- assertEquals(1,node.at("/keys").size());
+ assertEquals(1, node.at("/keys").size());
node = node.at("/keys/0");
assertEquals("RSA", node.at("/kty").asText());
assertEquals(config.getRsaKeyId(), node.at("/kid").asText());
assertNotNull(node.at("/e").asText());
assertNotNull(node.at("/n").asText());
}
-
+
@Test
public void testOpenIDConfiguration () throws KustvaktException {
ClientResponse response = resource().path("oauth2").path("openid")
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
index 9ff8db4..89b0f3c 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
@@ -178,6 +178,7 @@
.header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
.get(ClientResponse.class);
String entity = response.getEntity(String.class);
+ System.out.println(entity);
JsonNode node = JsonUtils.readTree(entity);
assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
assertEquals(StatusCodes.AUTHORIZATION_FAILED,
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index 9061918..6a6b5c2 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -62,7 +62,7 @@
openid.token.signing.algorithms = RS256
openid.subject.types = public
openid.display.types = page
-openid.supported.scopes = openid email
+openid.supported.scopes = openid email auth_time
openid.support.claim.param = false
openid.claim.types = normal
openid.supported.claims = iss sub aud exp iat
diff --git a/full/src/test/resources/test-oauth2.token b/full/src/test/resources/test-oauth2.token
deleted file mode 100644
index eb7b4af..0000000
--- a/full/src/test/resources/test-oauth2.token
+++ /dev/null
@@ -1 +0,0 @@
-249c64a77f40e2b5504982cc5521b596
\ No newline at end of file