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