AuthzInfo.java

/*******************************************************************************
 * Copyright (c) 2019, RISE AB
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions 
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *******************************************************************************/
package se.sics.ace.rs;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.eclipse.californium.oscore.CoapOSException;
import org.eclipse.californium.oscore.OSCoreCtxDB;

import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;

import org.eclipse.californium.cose.CoseException;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.Endpoint;
import se.sics.ace.Message;
import se.sics.ace.TimeProvider;
import se.sics.ace.Util;
import se.sics.ace.coap.rs.oscoreProfile.OscoreCtxDbSingleton;
import se.sics.ace.cwt.CWT;
import se.sics.ace.cwt.CwtCryptoCtx;

/**
 * This class implements the /authz_info endpoint at the RS that receives
 * access tokens, verifies if they are valid and then stores them.
 * 
 * Note this implementation requires the following claims in a CWT:
 * iss, sub, scope, aud.
 * 
 * @author Ludwig Seitz and Marco Tiloca
 *
 */
public class AuthzInfo implements Endpoint, AutoCloseable {
	
    /**
     * The logger
     */
    private static final Logger LOGGER 
        = Logger.getLogger(AuthzInfo.class.getName());

	/**
	 * The acceptable issuers
	 */
	private List<String> issuers;
	
	/**
	 * Provides system time
	 */
	private TimeProvider time;
	
	/**
	 * Handles introspection of tokens
	 */
	private IntrospectionHandler intro;
	
	/**
	 * Handles audience validation
	 */
	private AudienceValidator audience;
	
	/**
	 * The crypto context to use with the AS
	 */
	private CwtCryptoCtx ctx;
		
	/**
	 * Flag to indicate if we need to check cnonces
	 */
	private boolean checkCnonce;
	
	/**
	 * Each set of the list refers to a different size of Recipient IDs.
	 * The element with index 0 includes as elements Recipient IDs with size 1 byte.
	 */
	private static List<Set<Integer>> usedRecipientIds = new ArrayList<Set<Integer>>();
	
	/**
	 * Constructor. Needs an initialized TokenRepository.
	 * 
	 * @param issuers  the list of acceptable issuer of access tokens
	 * @param time  the time provider
	 * @param intro  the introspection handler (can be null)
	 * @param rsId  the identifier of the Resource Server
	 * @param audience  the audience validator
	 * @param ctx  the crypto context to use with the As
	 * @param keyDerivationKey  the key derivation key to use with the As, it can be null
	 * @param derivedKeySize  the size in bytes of symmetric keys derived with the key derivation key
	 * @param tokenFile  the file where to save tokens when persisting
	 * @param scopeValidator  the application specific scope validator 
	 * @param checkCnonce  true if this RS uses cnonces for freshness validation
	 * @throws AceException  if the token repository is not initialized
	 * @throws IOException 
	 */
	public AuthzInfo(List<String> issuers, 
			TimeProvider time, IntrospectionHandler intro, String rsId, 
			AudienceValidator audience, CwtCryptoCtx ctx, byte[] keyDerivationKey, int derivedKeySize,
			String tokenFile, ScopeValidator scopeValidator, boolean checkCnonce) 
			        throws AceException, IOException {
        if (TokenRepository.getInstance()==null) {     
            TokenRepository.create(scopeValidator, tokenFile, ctx, keyDerivationKey, derivedKeySize, time, rsId);
        }
		this.issuers = new ArrayList<>();
		this.issuers.addAll(issuers);
		this.time = time;
		this.intro = intro;
		this.audience = audience;
		this.ctx = ctx;
		this.checkCnonce = checkCnonce;
		
    	for (int i = 0; i < 4; i++) {
        	// Empty sets of assigned Sender IDs; one set for each possible Sender ID size in bytes.
        	// The set with index 0 refers to Sender IDs with size 1 byte
    		usedRecipientIds.add(new HashSet<Integer>());
    	}
    	
	}

	@Override
	public synchronized Message processMessage(Message msg) {
	    LOGGER.log(Level.INFO, "received message: " + msg);
	    CBORObject token = null;
	    try {
	        token = CBORObject.DecodeFromBytes(msg.getRawPayload());
	    } catch (Exception e) {
	        LOGGER.info("Invalid payload at authz-info: " + e.getMessage());
	        CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }
	    return processToken(token, msg);
	}
	
	protected synchronized Message processToken(CBORObject token,  Message msg) {
	    Map<Short, CBORObject> claims = null;
	    
        byte[] recipientId = null;
        boolean recipientIdFound = false;

		//1. Check whether it is a CWT or REF type
	    if (token.getType().equals(CBORType.ByteString)) {
	        try {
                claims = processReferenceToken(token);
            } catch (AceException e) {
                LOGGER.severe("Message processing aborted: " + e.getMessage());
                return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
            } catch (IntrospectionException e) {
                LOGGER.info("Introspection error, "
                         + "message processing aborted: " + e.getMessage());
                if (e.getMessage().isEmpty()) {
                    return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
                }
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                map.Add(Constants.ERROR_DESCRIPTION, e.getMessage());
                return msg.failReply(e.getCode(), map);
            }
	    } else if (token.getType().equals(CBORType.Array)) {
	        try {
	            claims = processCWT(token);
	        } catch (IntrospectionException e) {
                LOGGER.info("Introspection error, "
                        + "message processing aborted: " + e.getMessage());
               if (e.getMessage().isEmpty()) {
                   return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
               }
               CBORObject map = CBORObject.NewMap();
               map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
               map.Add(Constants.ERROR_DESCRIPTION, e.getMessage());
               return msg.failReply(e.getCode(), map);
	        } catch (AceException | CoseException 
	                | InvalidCipherTextException e) {
	            LOGGER.info("Token invalid: " + e.getMessage());
	            CBORObject map = CBORObject.NewMap();
	            map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
	            map.Add(Constants.ERROR_DESCRIPTION, "Token is invalid");
                return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	        } catch (Exception e) {
	            LOGGER.severe("Unsupported key wrap algorithm in token: " 
	                    + e.getMessage());
	            return msg.failReply(Message.FAIL_NOT_IMPLEMENTED, null);
            } 
	    } else {
	        CBORObject map = CBORObject.NewMap();
	        map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, "Unknown token format");
	        LOGGER.info("Message processing aborted: invalid request");
	        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }
	    
	    //2. Check if the token is active, this will only be present if we 
	    // did introspect
	    CBORObject active = claims.get(Constants.ACTIVE);
        if (active != null && active.isFalse()) {
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
            map.Add(Constants.ERROR_DESCRIPTION, "Token is not active");
            LOGGER.info("Message processing aborted: Token is not active");
            return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
        }

	    //3. Check that the token is not expired (exp)
	    CBORObject exp = claims.get(Constants.EXP);
	    if (exp != null && exp.AsNumber().ToInt64Checked() < this.time.getCurrentTime()) { 
	        CBORObject map = CBORObject.NewMap();
	        map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
            map.Add(Constants.ERROR_DESCRIPTION, "Token is expired");
            LOGGER.log(Level.INFO, "Message processing aborted: "
                    + "Token is expired");
	        return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
	    }   
      
	    //4. Check if we accept the issuer (iss)
	    CBORObject iss = claims.get(Constants.ISS);
	    if (iss != null) {
	        if (!this.issuers.contains(iss.AsString())) {
	            CBORObject map = CBORObject.NewMap();
	            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
	            map.Add(Constants.ERROR_DESCRIPTION, "Token issuer unknown");
	            LOGGER.log(Level.INFO, "Message processing aborted: "
	                    + "Token issuer unknown");
	            return msg.failReply(Message.FAIL_UNAUTHORIZED, map);
	        }
	    }
	    
	    //5. Check if we are the audience (aud)
	    CBORObject audCbor = claims.get(Constants.AUD);
	    if (audCbor == null) {
	        CBORObject map = CBORObject.NewMap();
	        map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
	        map.Add(Constants.ERROR_DESCRIPTION, "Token has no audience");
	        LOGGER.log(Level.INFO, "Message processing aborted: "
	                + "Token has no audience");
	        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }

	    String aud;
	    if (audCbor.getType().equals(CBORType.TextString)) {
	        aud = audCbor.AsString();   
	    } else {//Error
	        CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, "Audience malformed");
	        LOGGER.log(Level.INFO, "Message processing aborted: "
	                + "audience malformed");
	        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }
	    
	    boolean audMatch = false;
	    if (this.audience.match(aud)) {
            audMatch = true;
        }
	    if (!audMatch) { 
	        CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.UNAUTHORIZED_CLIENT);
            map.Add(Constants.ERROR_DESCRIPTION, "Audience does not apply");
            LOGGER.log(Level.INFO, "Message processing aborted: "
                    + "Audience does not apply");
	        return msg.failReply(Message.FAIL_FORBIDDEN, map);
	    }

	    //6. Check if the token has a scope
	    CBORObject scope = claims.get(Constants.SCOPE);
	    if (scope == null) {
	        CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_SCOPE);
            map.Add(Constants.ERROR_DESCRIPTION, "Token has no scope");
            LOGGER.log(Level.INFO, "Message processing aborted: "
                    + "Token has no scope");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }
	    
	    //7. Check if any part of the scope is meaningful to us
	    boolean meaningful = false;
	    try {
	    	if(scope.getType().equals(CBORType.TextString)) {
	    		meaningful = TokenRepository.getInstance().checkScope(scope);
	    	}
	    	else {
	    		// The version of checkScope() with two arguments is invoked
	    		// This is currently expecting a structured scope for joining OSCORE groups
	    		meaningful = TokenRepository.getInstance().checkScope(scope, aud);
	    	}
	    } catch (AceException e) {
	        LOGGER.info("Invalid scope, "
                    + "message processing aborted: " + e.getMessage());
	        CBORObject map = CBORObject.NewMap();
	        map.Add(Constants.ERROR, Constants.INVALID_SCOPE);
            map.Add(Constants.ERROR_DESCRIPTION, "Scope has invalid format");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map); 
	    }
	    if (!meaningful) {
	        CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_SCOPE);
            map.Add(Constants.ERROR_DESCRIPTION, "Scope does not apply");
            LOGGER.log(Level.INFO, "Message processing aborted: "
                    + "Token's scope does not apply");
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }
	    
	    //8. Handle EXI if present
	    int exiSeqNum = handleExi(claims);
	    if (exiSeqNum < -1) {
	    	// The 'exi' claim is present, but an error occurs during its processing
	        CBORObject map = CBORObject.NewMap();
	        String errStr = null;
	        switch (exiSeqNum) {
	        	case -2: // the 'cti' claim is not present in the Access Token as a CBOR byte string
	        		errStr = "The Access Token includes the 'exi' claim, but the 'cti' claim is not present as a CBOR byte string";
	        		break;
	        	case -3: // the 'cti' claim is present but it is not formatted as expected
	        		errStr = "The Access Token includes the 'exi' claim, but the 'cti' claim is not formatted as expected";
	        		break;	
	        	case -4: // the Sequence Number encoded in the 'cti' claim is not greater than the stored highest Sequence Number
	        		errStr = "The Access Token includes the 'exi' claim, but the Sequence Number value is too little";
	        }
	        map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, errStr);
            LOGGER.log(Level.INFO, "Message processing aborted: " + errStr);
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }
	    
	    //9. Handle cnonce if required
	    try {
	        handleCnonce(claims);
	    } catch (AceException e) {
	        CBORObject map = CBORObject.NewMap();
	        map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, e.getMessage());
            LOGGER.log(Level.INFO, "Message processing aborted: "
                    + "error while checking cnonce: " + e.getMessage());
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }
	    
		CBORObject cnf = claims.get(Constants.CNF);
		try {
	        if (cnf == null) {
	            LOGGER.severe("Token has not cnf");
	            throw new AceException("Token has no cnf");
	        }
		}
	    catch (Exception e) {
	        LOGGER.info("No Recipient ID available to use");
	        return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
	        
	    }
        
		boolean firstOscoreAccessToken = false;
		
	    // The OSCORE profile is being used. The Resource Server has to determine
	    // an available Recipient ID to offer to the Client.
    	if (cnf.getKeys().contains(Constants.OSCORE_Input_Material)) {
	    
    		if (msg.getSenderId() != null) {
    	        LOGGER.info("OSCORE Input Material provided over a protected POST request");
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                map.Add(Constants.ERROR_DESCRIPTION, 
                        "OSCORE_Input_Material provided over a protected POST request");
    	        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
    		}
    		
        	CBORObject osc = cnf.get(Constants.OSCORE_Input_Material);
        	try {
	            if (osc == null || !osc.getType().equals(CBORType.Map)) {
	                LOGGER.info("Missing or invalid parameter type for "
	                        + "'OSCORE_Input_Material', must be CBOR-map");
	                throw new AceException("invalid/missing OSCORE_Input_Material");
	            }
    		}
    	    catch (Exception e) {
    	        LOGGER.info("Invalid/missing OSCORE_Input_Material");
                CBORObject map = CBORObject.NewMap();
                map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
                map.Add(Constants.ERROR_DESCRIPTION, 
                        "invalid/missing OSCORE_Input_Material");
    	        return msg.failReply(Message.FAIL_BAD_REQUEST, map);
    	        
    	    }
    		
	    	CBORObject cbor = CBORObject.DecodeFromBytes(msg.getRawPayload());
	    	byte[] senderId = cbor.get(Constants.ACE_CLIENT_RECIPIENTID).GetByteString();
	    	
	        OSCoreCtxDB db = OscoreCtxDbSingleton.getInstance();
	        
	        // Determine an available Recipient ID to offer to the Client as ID2 (i.e., as Client's Sender ID)
	        synchronized(usedRecipientIds) {
	        	synchronized(db) {
	        	
		        	int maxIdValue;
		        	
	    			byte[] contextId = new byte[0];
	    			if (cnf.get(Constants.OSCORE_Input_Material).ContainsKey(Constants.OS_CONTEXTID)) {
	    				contextId = cnf.get(Constants.OSCORE_Input_Material).get(Constants.OS_CONTEXTID).GetByteString();
	    			}
		        	
			        // Start with 1 byte as size of Recipient ID; try with up to 4 bytes in size        
			        for (int idSize = 1; idSize <= 4; idSize++) {
			        	
			        	if (idSize == 4)
			        		maxIdValue = (1 << 31) - 1;
			        	else
			        		maxIdValue = (1 << (idSize * 8)) - 1;
			        	
				        for (int j = 0; j <= maxIdValue; j++) {
				        	
		        			recipientId = Util.intToBytes(j, idSize);
		        			
		        			// The Recipient ID must be different than what offered by the Client in the 'id1' parameter
		        			if(Arrays.equals(senderId, recipientId))
		        				continue;
		        			
		        			// This Recipient ID is marked as not available to use
		        			if (usedRecipientIds.get(idSize - 1).contains(j))
		        				continue;
		        			
		        			try {
					        	// This Recipient ID seems to be available to use 
				        		if (!usedRecipientIds.get(idSize - 1).contains(j)) {
				        			
				        			// Double check in the database of OSCORE Security Contexts
				        			if (db.getContext(recipientId,  contextId) != null) {
				        				
				        				// A Security Context with this Recipient ID exists and was not tracked!
				        				// Update the local list of used Recipient IDs, then move on to the next candidate
				        				usedRecipientIds.get(idSize - 1).add(j);
				        				continue;
				        				
				        			}
				        			else {
				        				
				        				// This Recipient ID is actually available at the moment. Add it to the local list
				        				usedRecipientIds.get(idSize - 1).add(j);
				        				recipientIdFound = true;
				        				break;
				        			}
				        			
				        		}
		        			}
			        		catch(RuntimeException e) {
		        				// Multiple Security Contexts with this Recipient ID exist and it was not tracked!
		        				// Update the local list of used Recipient IDs, then move on to the next candidate
		        				usedRecipientIds.get(idSize - 1).add(j);
		        				continue;
			        		} catch (CoapOSException e) {
			    		        LOGGER.severe("Error while accessing the database of OSCORE Security Contexts");
			    	            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
							}
			        			
				        }
				        
				        if (recipientIdFound)
				        	break;
				        	
			        }
	        	}
	        }
	        
		    try {
		    	if (!recipientIdFound) {
		            throw new AceException("No Recipient ID available to use");
		        }
		    } catch (Exception e) {
		        LOGGER.info("No Recipient ID available to use");
	            return msg.failReply(Message.FAIL_INTERNAL_SERVER_ERROR, null);
	            
		    }
	
	    	claims.get(Constants.CNF).get(Constants.OSCORE_Input_Material).Add(Constants.OS_CLIENTID, CBORObject.FromObject(recipientId));
	    	claims.get(Constants.CNF).get(Constants.OSCORE_Input_Material).Add(Constants.OS_SERVERID, CBORObject.FromObject(senderId));
		    
	    	// This is not a Token for updating access rights,
	    	// but rather to establish a new OSCORE Security Context
	    	firstOscoreAccessToken = true;
	    	
	    }
	    
	    //9. Extension point for handling other special claims in the future
	    processOther(claims);
	    
	    //10. Store the claims of this token
	    CBORObject cti = null;
	    
	    //Check if we have a sid
	    String sid = msg.getSenderId();
	    try {
            cti = TokenRepository.getInstance().addToken(token, claims, this.ctx, sid, exiSeqNum);
        } catch (AceException e) {
            LOGGER.severe("Message processing aborted: " + e.getMessage());
            CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, e.getMessage());
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
        }
	    
	    //11. Create success message
	    //Return the cti or the local identifier assigned to the token
	    CBORObject rep = CBORObject.NewMap();
	    rep.Add(Constants.CTI, cti);
	    
	    // The following enables this class to return to the specific AuthzInfo instance also the
	    // Sender Identifier associated to this Access Token, as 'SUB' parameter of the response.
	    String assignedKid = null;
	    String assignedSid;
	    try {
	    	String ctiStr = Base64.getEncoder().encodeToString(cti.GetByteString());
	    	
	    	cnf = claims.get(Constants.CNF);
	    		    	
	    	// This should really not happen for a previously validated and stored Access Token
	    	if (cnf == null) {
	            LOGGER.severe("Token has not cnf");
	            throw new AceException("Token has no cnf");
	        }
	    	// This should really not happen for a previously validated and stored Access Token
	    	if (!cnf.getType().equals(CBORType.Map)) {
	            LOGGER.severe("Malformed cnf in token");
	            throw new AceException("cnf claim malformed in token");
	        }
	    	
	    	// Rebuild the 'kid' leveraging what already happened in the Token Repository
	    	assignedKid = TokenRepository.getInstance().getKidByCti(ctiStr);
	    	
	    	// This should really not happen for a previously validated and stored Access Token
	    	if (assignedKid == null) {
	            LOGGER.severe("kid not found");
	            throw new AceException("kid not found");
	        }
	    	
	    	assignedSid = TokenRepository.getInstance().getSid(assignedKid);
	    }
	    catch (Exception e) {
	    	LOGGER.info("Unable to retrieve kid after token addition: " + e.getMessage());
	        CBORObject map = CBORObject.NewMap();
            map.Add(Constants.ERROR, Constants.INVALID_REQUEST);
            map.Add(Constants.ERROR_DESCRIPTION, e.getMessage());
            return msg.failReply(Message.FAIL_BAD_REQUEST, map);
	    }

	    if (assignedSid != null)
	    	rep.Add(Constants.SUB, assignedSid);

	    // If the OSCORE profile is being used, and especially a new OSCORE Security Context is
	    // being established, return also the selected Recipient ID to the specific AuthzInfo instance.
	    //
	    // This is not the case when a Token is posted to update access rights, by superseding an
	    // existing Token from which the original 'cnf' claim is inherited and retained in the new Token
    	if (firstOscoreAccessToken == true) {
    		
    		String recipientIdString = Base64.getEncoder().encodeToString(recipientId);
	    	rep.Add(Constants.CLIENT_ID, recipientIdString);
	    	
    	}
    	
	    LOGGER.info("Successfully processed token");
        return msg.successReply(Message.CREATED, rep);
	}
	
	/**
	 * Extension point for handling other special claims.
	 * 
	 * @param claims all claims
	 */
	protected synchronized void processOther(Map<Short, CBORObject> claims) {
	    //No processing needed
    }

    /**
	 * Process a message containing a CWT.
	 * 
	 * Note: The behavior implemented here is the following:
	 * If we have an introspection handler, we try to introspect,
	 * if introspection fails we just return the claims from the CWT,
	 * otherwise we add the claims returned by introspection 
	 * to those of the CWT, possibly overwriting CWT claims with
	 * "fresher" introspection claim having the same id.
	 * 
	 * @param token  the token as CBOR
	 * 
	 * @return  the claims of the CWT
	 * 
	 * @throws AceException 
	 * @throws IntrospectionException 
	 * @throws CoseException
	 * 
	 * @throws Exception  when using a not supported key wrap
	 */
	protected synchronized Map<Short,CBORObject> processCWT(CBORObject token)
	        throws IntrospectionException, AceException, 
	        CoseException, Exception {
	    CWT cwt = CWT.processCOSE(token.EncodeToBytes(), this.ctx);
	    //Check if we can introspect this token
	    Map<Short, CBORObject> claims = cwt.getClaims();
	   if (this.intro != null) {
	       CBORObject cti = claims.get(Constants.CTI);
	       if (cti != null && cti.getType().equals(CBORType.ByteString)) {
	           Map<Short, CBORObject> introClaims 
	               = this.intro.getParams(cti.GetByteString());
	           if (introClaims != null) {
	               claims.putAll(introClaims);
	           }
	       }
	   }
	   return claims;
    }
    
	/**
	 * Process a message containing a reference token.
	 * 
	 * @param token  the token as CBOR
	 * 
	 * @return  the claims of the reference token
	 * @throws AceException
	 * @throws IntrospectionException 
	 */
    protected synchronized Map<Short, CBORObject> processReferenceToken(CBORObject token)
                throws AceException, IntrospectionException {
		// This should be a CBOR String
        if (token.getType() != CBORType.ByteString) {
            throw new AceException("Reference Token processing error");
        }
        
        // Try to introspect the token
        if (this.intro == null) {
            throw new AceException("Introspection handler not found");
        }
        Map<Short, CBORObject> params 
            = this.intro.getParams(token.GetByteString());        
        if (params == null) {
            params = new HashMap<>();
            params.put(Constants.ACTIVE, CBORObject.False);
        }
       
        return params;
	}
    
    /**
     * Handle exi claim, if present.
     * This is done by internally translating it to a exp claim in sync with the local time.
     * 
     * Additional checks are also performed, to ensure that the Sequence Number
     * encoded in the 'cti' claim is strictly greater than the highest Sequence Number
     * received by this Resource Server in Access Tokens that include the 'exi' claim.
     * 
     * @param claims
     * 
     * @return  It returns a positive integer if the Sequence Number is successfully extracted from the 'cti' claim
     * 		    It returns a negative integer in the following cases:
     * 			-1 : the 'exi' claim is not present
     * 		    -2 : the 'cti' claim is not present in the Access Token as a CBOR byte string
     *          -3 : the 'cti' claim is present but it is not formatted as expected
     *          -4 : the Sequence Number encoded in the 'cti' claim is not greater than the stored highest Sequence Number
     */
    private synchronized int handleExi(Map<Short, CBORObject> claims) {
    	
        CBORObject exi = claims.get(Constants.EXI);
        if (exi == null) {
        	return -1;
        }
        
        // Determine the expiration time and add it to the Access Token as value of an 'exp' claim
        Long now = this.time.getCurrentTime();
        Long exp = now + exi.AsNumber().ToInt64Checked();
        claims.put(Constants.EXP, CBORObject.FromObject(exp));
        
        // Check that the 'cti' claim is also present
        CBORObject cticb = claims.get(Constants.CTI);
        if (cticb == null || cticb.getType() != CBORType.ByteString) {
        	// The 'cti' claim is not included in the Access Token as a CBOR byte string.
    		return -2;
        }
        
        // Retrieve the Sequence Number from the 'cti' claim
        int seqNum = TokenRepository.getInstance().getExiSeqNumFromCti(cticb.GetByteString());
        
        if (seqNum < 0) {
        	// The 'cti' claim is malformed
        	return -3;
        }        
        if (seqNum <= TokenRepository.getInstance().getTopExiSequenceNumber()) {
        	// The Sequence Number encoded in the 'cti' claim is not greater than the stored highest Sequence Number
        	return -4;
        }
        
        return seqNum;
        
    }
    
    /**
     * Handle cnonce if required
     * @throws AceException 
     */
    private synchronized void handleCnonce(Map<Short, CBORObject> claims) throws AceException {
        if (this.checkCnonce) {
            CnonceHandler.getInstance().checkNonce(claims);
        }
    }
    
    @Override
    public void close() throws AceException {
        if (TokenRepository.getInstance() != null) {
            TokenRepository.getInstance().close();
        }       
    }	
}