changeset 72:e381ba725669

new PUT auth/tokens/{token-type} API route + updated interface with WebService
author Alain Mazy <am@osimis.io>
date Fri, 24 Feb 2023 18:13:36 +0100
parents 30fb3ce960d9
children 512247750f0a
files NEWS Plugin/AuthorizationWebService.cpp Plugin/AuthorizationWebService.h Plugin/CachedAuthorizationService.cpp Plugin/CachedAuthorizationService.h Plugin/DefaultConfiguration.json Plugin/IAuthorizationService.h Plugin/Plugin.cpp README
diffstat 9 files changed, 481 insertions(+), 103 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Wed Feb 22 13:13:38 2023 +0100
+++ b/NEWS	Fri Feb 24 18:13:36 2023 +0100
@@ -1,7 +1,12 @@
-* new "orthanc-explorer-2" StandardConfigurations
-* new "auth/user-profile" Rest API route
+* BREAKING-CHANGE: the API between the authorization plugin and the 
+  WebService has slightly changed.  Check the samples in the README (TODO).
+  - "identifier" has been renamed into "server-id"
 * new user-permission based authorization model.  This is enabled if you
   define the new "WebServiceUserProfileUrl" configuration.
+* new "orthanc-explorer-2" StandardConfigurations
+* new GET "auth/user/profile" Rest API route to retrieve user permissions
+* new PUT "auth/tokens/{token-type}" Rest API route to create tokens
+
 
 2022-11-16 - v 0.4.1
 ====================
--- a/Plugin/AuthorizationWebService.cpp	Wed Feb 22 13:13:38 2023 +0100
+++ b/Plugin/AuthorizationWebService.cpp	Fri Feb 24 18:13:36 2023 +0100
@@ -82,15 +82,14 @@
 
     if (!identifier_.empty())
     {
-      body["identifier"] = identifier_;
+      body["server-id"] = identifier_;
     }
     else
     {
-      body["identifier"] = Json::nullValue;
+      body["server-id"] = Json::nullValue;
     }
 
     Orthanc::WebServiceParameters authWebservice;
-    authWebservice.SetUrl(url_);
 
     if (!username_.empty())
     {
@@ -101,6 +100,7 @@
     Orthanc::Toolbox::WriteFastJson(bodyAsString, body);
 
     Orthanc::HttpClient authClient(authWebservice, "");
+    authClient.SetUrl(tokenValidationUrl_);
     authClient.AssignBody(bodyAsString);
     authClient.SetMethod(Orthanc::HttpMethod_Post);
     authClient.AddHeader("Content-Type", "application/json");
@@ -158,16 +158,96 @@
     password_ = password;
   }
 
-  void AuthorizationWebService::SetUserProfileUrl(const std::string& url)
-  {
-    userProfileUrl_ = url;
-  }
-
   void AuthorizationWebService::SetIdentifier(const std::string& webServiceIdentifier)
   {
     identifier_ = webServiceIdentifier;
   }
 
+  bool AuthorizationWebService::CreateToken(IAuthorizationService::CreatedToken& response,
+                                            const std::string& tokenType, 
+                                            const std::string& id, 
+                                            const std::vector<IAuthorizationService::OrthancResource>& resources,
+                                            const std::string& expirationDateString)
+  {
+    if (tokenCreationBaseUrl_.empty())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest, "Can not create tokens if the 'WebServiceTokenCreationBaseUrl' is not configured");
+    }
+    std::string url = Orthanc::Toolbox::JoinUri(tokenCreationBaseUrl_, tokenType);
+
+    Orthanc::WebServiceParameters authWebservice;
+
+    if (!username_.empty())
+    {
+      authWebservice.SetCredentials(username_, password_);
+    }
+
+    Json::Value body;
+
+    if (!id.empty())
+    {
+      body["id"] = id;
+    }
+
+    body["resources"] = Json::arrayValue;
+    for (size_t i = 0; i < resources.size(); ++i)
+    {
+      Json::Value resource;
+      if (!resources[i].dicomUid.empty())
+      {
+        resource["dicom-uid"] = resources[i].dicomUid;
+      }
+      if (!resources[i].orthancId.empty())
+      {
+        resource["orthanc-id"] = resources[i].orthancId;
+      }
+      if (!resources[i].url.empty())
+      {
+        resource["url"] = resources[i].url;
+      }
+      if (!resources[i].level.empty())
+      {
+        resource["level"] = resources[i].level;
+      }
+
+      body["resources"].append(resource);
+    }
+
+    body["type"] = tokenType;
+    if (!expirationDateString.empty())
+    {
+      body["expiration-date"] = expirationDateString;
+    }
+
+    std::string bodyAsString;
+    Orthanc::Toolbox::WriteFastJson(bodyAsString, body);
+
+    Json::Value tokenResponse;
+    try
+    {
+      Orthanc::HttpClient authClient(authWebservice, "");
+      authClient.SetUrl(url);
+      authClient.AssignBody(bodyAsString);
+      authClient.SetMethod(Orthanc::HttpMethod_Put);
+      authClient.AddHeader("Content-Type", "application/json");
+      authClient.AddHeader("Expect", "");
+      authClient.SetTimeout(10);
+
+      authClient.ApplyAndThrowException(tokenResponse);
+
+      response.token = tokenResponse["token"].asString();
+      response.url = tokenResponse["url"].asString();
+
+      return true;
+    }
+    catch (Orthanc::OrthancException& ex)
+    {
+      return false;
+    }
+
+  }
+
+
   bool AuthorizationWebService::GetUserProfileInternal(unsigned int& validity,
                                                        Json::Value& profile /* out */,
                                                        const Token* token,
@@ -179,7 +259,6 @@
     }
 
     Orthanc::WebServiceParameters authWebservice;
-    authWebservice.SetUrl(userProfileUrl_);
 
     if (!username_.empty())
     {
@@ -209,6 +288,7 @@
     try
     {
       Orthanc::HttpClient authClient(authWebservice, "");
+      authClient.SetUrl(userProfileUrl_);
       authClient.AssignBody(bodyAsString);
       authClient.SetMethod(Orthanc::HttpMethod_Post);
       authClient.AddHeader("Content-Type", "application/json");
--- a/Plugin/AuthorizationWebService.h	Wed Feb 22 13:13:38 2023 +0100
+++ b/Plugin/AuthorizationWebService.h	Fri Feb 24 18:13:36 2023 +0100
@@ -26,11 +26,12 @@
   class AuthorizationWebService : public BaseAuthorizationService
   {
   private:
-    std::string url_;
     std::string username_;
     std::string password_;
     std::string identifier_;
     std::string userProfileUrl_;
+    std::string tokenValidationUrl_;
+    std::string tokenCreationBaseUrl_;
 
   protected:
     virtual bool IsGrantedInternal(unsigned int& validity,
@@ -50,8 +51,12 @@
                                    const std::string& tokenValue) ORTHANC_OVERRIDE;
   
   public:
-    AuthorizationWebService(const std::string& url) :
-      url_(url)
+    AuthorizationWebService(const std::string& tokenValidationUrl, 
+                            const std::string& tokenCreationBaseUrl, 
+                            const std::string& userProfileUrl) :
+      userProfileUrl_(userProfileUrl),
+      tokenValidationUrl_(tokenValidationUrl),
+      tokenCreationBaseUrl_(tokenCreationBaseUrl)
     {
     }
 
@@ -60,6 +65,26 @@
 
     void SetIdentifier(const std::string& webServiceIdentifier);
 
-    void SetUserProfileUrl(const std::string& url);
+    virtual bool HasUserProfile() const
+    {
+      return !userProfileUrl_.empty();
+    }
+
+    virtual bool HasCreateToken() const
+    {
+      return !tokenCreationBaseUrl_.empty();
+    }
+
+    virtual bool HasTokenValidation() const
+    {
+      return !tokenValidationUrl_.empty();
+    }
+
+    virtual bool CreateToken(IAuthorizationService::CreatedToken& response,
+                             const std::string& tokenType, 
+                             const std::string& id, 
+                             const std::vector<IAuthorizationService::OrthancResource>& resources,
+                             const std::string& expirationDateString) ORTHANC_OVERRIDE;
+
   };
 }
--- a/Plugin/CachedAuthorizationService.cpp	Wed Feb 22 13:13:38 2023 +0100
+++ b/Plugin/CachedAuthorizationService.cpp	Fri Feb 24 18:13:36 2023 +0100
@@ -27,20 +27,36 @@
 {
   std::string CachedAuthorizationService::ComputeKey(OrthancPluginHttpMethod method,
                                                      const AccessedResource& access,
-                                                     const Token& token,
+                                                     const Token* token,
                                                      const std::string& tokenValue) const
   {
-    return (boost::lexical_cast<std::string>(method) + "|" +
-            boost::lexical_cast<std::string>(access.GetLevel()) + "|" +
-            access.GetOrthancId() + "|" + token.GetKey() + "|" + tokenValue);
+    if (token != NULL)
+    {
+      return (boost::lexical_cast<std::string>(method) + "|" +
+              boost::lexical_cast<std::string>(access.GetLevel()) + "|" +
+              access.GetOrthancId() + "|" + token->GetKey() + "|" + tokenValue);
+    }
+    else
+    {
+      return (boost::lexical_cast<std::string>(method) + "|" +
+              boost::lexical_cast<std::string>(access.GetLevel()) + "|" +
+              access.GetOrthancId() + "|anonymous");
+    }
   }
     
 
   std::string CachedAuthorizationService::ComputeKey(const std::string& permission,
-                                                     const Token& token,
+                                                     const Token* token,
                                                      const std::string& tokenValue) const
   {
-    return (permission + "|" + token.GetKey() + "|" + tokenValue);
+    if (token != NULL)
+    {
+      return (permission + "|" + token->GetKey() + "|" + tokenValue);
+    }
+    else
+    {
+      return (permission + "|anonymous");
+    }
   }
 
 
@@ -64,7 +80,7 @@
   {
     assert(decorated_.get() != NULL);
 
-    std::string key = ComputeKey(method, access, *token, tokenValue);
+    std::string key = ComputeKey(method, access, token, tokenValue);
     std::string value;
 
     if (cache_->Retrieve(value, key))
@@ -112,7 +128,7 @@
   {
     assert(decorated_.get() != NULL);
 
-    std::string key = ComputeKey(permission, *token, tokenValue);
+    std::string key = ComputeKey(permission, token, tokenValue);
     std::string value;
 
     if (cache_->Retrieve(value, key))
--- a/Plugin/CachedAuthorizationService.h	Wed Feb 22 13:13:38 2023 +0100
+++ b/Plugin/CachedAuthorizationService.h	Fri Feb 24 18:13:36 2023 +0100
@@ -38,11 +38,11 @@
 
     std::string ComputeKey(OrthancPluginHttpMethod method,
                            const AccessedResource& access,
-                           const Token& token,
+                           const Token* token,
                            const std::string& tokenValue) const;
 
     std::string ComputeKey(const std::string& permission,
-                           const Token& token,
+                           const Token* token,
                            const std::string& tokenValue) const;
 
     virtual bool IsGrantedInternal(unsigned int& validity,
@@ -66,5 +66,33 @@
     CachedAuthorizationService(BaseAuthorizationService* decorated /* takes ownership */,
                                ICacheFactory& factory);
 
+    virtual bool HasUserProfile() const
+    {
+      return decorated_->HasUserProfile();
+    }
+
+    virtual bool HasCreateToken() const
+    {
+      return decorated_->HasCreateToken();
+    }
+
+    virtual bool HasTokenValidation() const
+    {
+      return decorated_->HasTokenValidation();
+    }
+
+    bool CreateToken(IAuthorizationService::CreatedToken& response,
+                     const std::string& tokenType, 
+                     const std::string& id, 
+                     const std::vector<IAuthorizationService::OrthancResource>& resources,
+                     const std::string& expirationDateString)
+    {
+      return decorated_->CreateToken(response,
+                                     tokenType,
+                                     id,
+                                     resources,
+                                     expirationDateString);
+    }
+
  };
 }
--- a/Plugin/DefaultConfiguration.json	Wed Feb 22 13:13:38 2023 +0100
+++ b/Plugin/DefaultConfiguration.json	Fri Feb 24 18:13:36 2023 +0100
@@ -1,10 +1,21 @@
 {
     "Authorization" : {
-        // The URL of the auth webservice implementing resource level authorization (optional if not implementing resource based permissions)
-        // "WebService" : "http://change-me:8000/validate",
-        
-        // The URL of the auth webservice implementing resource level authorization (optional if not implementing user-permissions)
-        // "WebServiceUserProfileUrl" : "http://change-me:8000/user-profile",
+        // The Base URL of the auth webservice.  This is an alias for all 3 next configurations:
+        // // "WebServiceUserProfileUrl" : " ROOT /user/get-profile",
+        // // "WebServiceTokenValidationUrl" : " ROOT /tokens/validate",
+        // // "WebServiceTokenCreationBaseUrl" : " ROOT /tokens/",
+        // "WebServiceRootUrl" : "http://change-me:8000/",
+
+        // The URL of the auth webservice route implementing user profile (optional)
+        // (this configuration was previously named "WebService" and its old name is still accepted
+        //  for backward compatibility)
+        // "WebServiceUserProfileUrl" : "http://change-me:8000/user/profile",
+
+        // The URL of the auth webservice route implementing resource level authorization (optional)
+        // "WebServiceTokenValidationUrl" : "http://change-me:8000/tokens/validate",
+
+        // The Base URL of the auth webservice route to create tokens (optional)
+        // "WebServiceTokenCreationBaseUrl" : "http://change-me:8000/tokens/",
 
         // The username and password to connect to the webservice (optional)
         //"WebServiceUsername": "change-me",
@@ -32,7 +43,7 @@
         //"UncheckedLevels" : [],
 
         // Definition of required "user-permissions".  This can be fully customized.
-        // You may define other permissions yourself as long as they mathc the permissions
+        // You may define other permissions yourself as long as they match the permissions
         // provided in the user-profile route implemented by the auth-service.
         // You may test your regex in https://regex101.com/ by selecting .NET (C#) and removing the leading ^ and trailing $
         // The default configuration is suitable for Orthanc-Explorer-2 (see TBD sample)
--- a/Plugin/IAuthorizationService.h	Wed Feb 22 13:13:38 2023 +0100
+++ b/Plugin/IAuthorizationService.h	Fri Feb 24 18:13:36 2023 +0100
@@ -31,6 +31,20 @@
   class IAuthorizationService : public boost::noncopyable
   {
   public:
+    struct OrthancResource
+    {
+      std::string dicomUid;
+      std::string orthancId;
+      std::string url;
+      std::string level;
+    };
+
+    struct CreatedToken
+    {
+      std::string url;
+      std::string token;
+    };
+
     virtual ~IAuthorizationService()
     {
     }
@@ -60,5 +74,15 @@
 
     virtual bool HasAnonymousUserPermission(unsigned int& validity /* out */,
                                             const std::set<std::string>& anyOfPermissions) = 0;
+
+    virtual bool CreateToken(CreatedToken& response,
+                             const std::string& tokenType, 
+                             const std::string& id, 
+                             const std::vector<OrthancResource>& resources,
+                             const std::string& expirationDateString) = 0;
+
+    virtual bool HasUserProfile() const = 0;
+    virtual bool HasCreateToken() const = 0;
+    virtual bool HasTokenValidation() const = 0;
   };
 }
--- a/Plugin/Plugin.cpp	Wed Feb 22 13:13:38 2023 +0100
+++ b/Plugin/Plugin.cpp	Fri Feb 24 18:13:36 2023 +0100
@@ -49,6 +49,19 @@
   return out;
 }
 
+struct TokenAndValue
+{
+  const OrthancPlugins::Token& token;
+  std::string value;
+
+  TokenAndValue(const OrthancPlugins::Token& token, const std::string& value) :
+    token(token),
+    value(value)
+  {
+  }
+};
+
+
 static int32_t FilterHttpRequests(OrthancPluginHttpMethod method,
                                   const char *uri,
                                   const char *ip,
@@ -61,6 +74,8 @@
 {
   try
   {
+    unsigned int validity;  // ignored
+
     if (method == OrthancPluginHttpMethod_Get)
     {
       // Allow GET accesses to static resources
@@ -79,7 +94,35 @@
       }
     }
 
-    unsigned int validity;  // ignored
+    OrthancPlugins::AssociativeArray headers(headersCount, headersKeys, headersValues, false);
+    OrthancPlugins::AssociativeArray getArguments(getArgumentsCount, getArgumentsKeys, getArgumentsValues, true);
+
+    std::vector<TokenAndValue> authTokens;  // the tokens that are set in this request
+
+    for (std::set<OrthancPlugins::Token>::const_iterator token = tokens_.begin(); token != tokens_.end(); ++token)
+    {
+      std::string value;
+
+      bool hasValue = false;
+      switch (token->GetType())
+      {
+        case OrthancPlugins::TokenType_HttpHeader:
+          hasValue = headers.GetValue(value, token->GetKey());
+          break;
+
+        case OrthancPlugins::TokenType_GetArgument:
+          hasValue = getArguments.GetValue(value, token->GetKey());
+          break;
+
+        default:
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      }
+      
+      if (hasValue)
+      {
+        authTokens.push_back(TokenAndValue(*token, value));
+      }
+    }
 
     // check if the user permissions grants him access
     if (permissionParser_.get() != NULL &&
@@ -90,7 +133,7 @@
       std::string matchedPattern;
       if (permissionParser_->Parse(requiredPermissions, matchedPattern, method, uri))
       {
-        if (tokens_.empty())
+        if (authTokens.empty())
         {
           LOG(INFO) << "Testing whether anonymous user has any of the required permissions '" << JoinStrings(requiredPermissions) << "'";
           if (authorizationService_->HasAnonymousUserPermission(validity, requiredPermissions))
@@ -100,25 +143,12 @@
         }
         else
         {
-          OrthancPlugins::AssociativeArray headers
-            (headersCount, headersKeys, headersValues, false);
-
-          // Loop over all the authorization tokens stored in the HTTP
-          // headers, until finding one that is granted
-          for (std::set<OrthancPlugins::Token>::const_iterator
-                  token = tokens_.begin(); token != tokens_.end(); ++token)
+          for (size_t i = 0; i < authTokens.size(); ++i)
           {
-            std::string value;
-
-            // we consider that users only works with HTTP Header tokens, not tokens from GetArgument
-            if (token->GetType() == OrthancPlugins::TokenType_HttpHeader &&
-              headers.GetValue(value, token->GetKey()))
+            LOG(INFO) << "Testing whether user has the required permission '" << JoinStrings(requiredPermissions) << "' based on the '" << authTokens[i].token.GetKey() << "' HTTP header required to match '" << matchedPattern << "'";
+            if (authorizationService_->HasUserPermission(validity, requiredPermissions, authTokens[i].token, authTokens[i].value))
             {
-              LOG(INFO) << "Testing whether user has the required permission '" << JoinStrings(requiredPermissions) << "' based on the '" << token->GetKey() << "' HTTP header required to match '" << matchedPattern << "'";
-              if (authorizationService_->HasUserPermission(validity, requiredPermissions, *token, value))
-              {
-                return 1;
-              }
+              return 1;
             }
           }
         }
@@ -130,7 +160,6 @@
     {
       // Parse the resources that are accessed through this URI
       OrthancPlugins::IAuthorizationParser::AccessedResources accesses;
-      OrthancPlugins::AssociativeArray getArguments(getArgumentsCount, getArgumentsKeys, getArgumentsValues, true);
 
       if (!authorizationParser_->Parse(accesses, uri, getArguments.GetMap()))
       {
@@ -152,39 +181,16 @@
 
           bool granted = false;
 
-          if (tokens_.empty())
+          if (authTokens.empty())
           {
             granted = authorizationService_->IsGrantedToAnonymousUser(validity, method, *access);
           }
           else
           {
-            OrthancPlugins::AssociativeArray headers
-              (headersCount, headersKeys, headersValues, false);
-
-            // Loop over all the authorization tokens stored in the HTTP
-            // headers, until finding one that is granted
-            for (std::set<OrthancPlugins::Token>::const_iterator
-                   token = tokens_.begin(); token != tokens_.end(); ++token)
+            // Loop over all the authorization tokens in the request until finding one that is granted
+            for (size_t i = 0; i < authTokens.size(); ++i)
             {
-              std::string value;
-
-              bool hasValue = false;
-              switch (token->GetType())
-              {
-                case OrthancPlugins::TokenType_HttpHeader:
-                  hasValue = headers.GetValue(value, token->GetKey());
-                  break;
-
-                case OrthancPlugins::TokenType_GetArgument:
-                  hasValue = getArguments.GetValue(value, token->GetKey());
-                  break;
-
-                default:
-                  throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
-              }
-              
-              if (hasValue &&
-                  authorizationService_->IsGranted(validity, method, *access, *token, value))
+              if (authorizationService_->IsGranted(validity, method, *access, authTokens[i].token, authTokens[i].value))
               {
                 granted = true;
                 break;
@@ -296,6 +302,103 @@
   }
 }
 
+void CreateToken(OrthancPluginRestOutput* output,
+                 const char* /*url*/,
+                 const OrthancPluginHttpRequest* request)
+{
+  OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+
+  if (request->method != OrthancPluginHttpMethod_Put)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "PUT");
+  }
+  else
+  {
+    // The filtering to this route is performed by this plugin as it is done for any other route before we get here.
+    // Since the route contains the tokenType, we can allow/forbid creating them based on the url
+
+    // simply forward the request to the auth-service
+    std::string tokenType;
+    if (request->groupsCount == 1)
+    {
+      tokenType = request->groups[0];
+    }
+    else
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    }
+
+    // convert from Orthanc flavored API to WebService API
+    Json::Value body;
+    if (!OrthancPlugins::ReadJson(body, request->body, request->bodySize))
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "A JSON payload was expected");
+    }
+
+    std::string id;
+    std::vector<OrthancPlugins::IAuthorizationService::OrthancResource> resources;
+    std::string expirationDateString;
+
+    if (body.isMember("ID"))
+    {
+      id = body["ID"].asString();
+    }
+
+    for (Json::ArrayIndex i = 0; i < body["Resources"].size(); ++i)
+    {
+      const Json::Value& jsonResource = body["Resources"][i];
+      OrthancPlugins::IAuthorizationService::OrthancResource resource;
+
+      if (jsonResource.isMember("DicomUid"))
+      {
+        resource.dicomUid = jsonResource["DicomUid"].asString();
+      }
+
+      if (jsonResource.isMember("OrthancId"))
+      {
+        resource.orthancId = jsonResource["OrthancId"].asString();
+      }
+
+      if (jsonResource.isMember("Url"))
+      {
+        resource.url = jsonResource["Url"].asString();
+      }
+
+      resource.level = jsonResource["Level"].asString();
+      resources.push_back(resource);
+    }
+
+    if (body.isMember("ExpirationDate"))
+    {
+      expirationDateString = body["ExpirationDate"].asString();
+    }
+
+    OrthancPlugins::IAuthorizationService::CreatedToken createdToken;
+    if (authorizationService_->CreateToken(createdToken,
+                                           tokenType,
+                                           id,
+                                           resources,
+                                           expirationDateString))
+    {
+      Json::Value createdJsonToken;
+      createdJsonToken["Token"] = createdToken.token;
+      
+      if (!createdToken.url.empty())
+      {
+        createdJsonToken["Url"] = createdToken.url;
+      }
+      else
+      {
+        createdJsonToken["Url"] = Json::nullValue;
+      }
+
+      OrthancPlugins::AnswerJson(createdJsonToken, output);
+    }
+    
+
+  }
+}
+
 void GetUserProfile(OrthancPluginRestOutput* output,
                     const char* /*url*/,
                     const OrthancPluginHttpRequest* request)
@@ -314,7 +417,6 @@
     OrthancPlugins::AssociativeArray getArguments
       (request->getCount, request->getKeys, request->getValues, true);
 
-
     // Loop over all the authorization tokens stored in the HTTP
     // headers, until finding one that is granted
     for (std::set<OrthancPlugins::Token>::const_iterator
@@ -477,42 +579,78 @@
         pluginConfiguration.LookupSetOfStrings(uncheckedResources_, "UncheckedResources", false);
         pluginConfiguration.LookupListOfStrings(uncheckedFolders_, "UncheckedFolders", false);
 
-        std::string url;
+        std::string urlTokenValidation;
+        std::string urlTokenCreationBase;
+        std::string urlUserProfile;
+        std::string urlRoot;
 
-        static const char* WEB_SERVICE = "WebService";
-        if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE))
+        static const char* WEB_SERVICE_ROOT = "WebServiceRootUrl";
+        static const char* WEB_SERVICE_TOKEN_VALIDATION = "WebServiceTokenValidationUrl";
+        static const char* WEB_SERVICE_TOKEN_CREATION_BASE = "WebServiceTokenCreationBaseUrl";
+        static const char* WEB_SERVICE_USER_PROFILE = "WebServiceUserProfileUrl";
+        static const char* WEB_SERVICE_TOKEN_VALIDATION_LEGACY = "WebService";
+        if (pluginConfiguration.LookupStringValue(urlRoot, WEB_SERVICE_ROOT))
         {
-          LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE << "\" configuration provided.  Will not perform resource based authorization.";
+          urlTokenValidation = Orthanc::Toolbox::JoinUri(urlRoot, "/tokens/validate");
+          urlTokenCreationBase = Orthanc::Toolbox::JoinUri(urlRoot, "/tokens/");
+          urlUserProfile = Orthanc::Toolbox::JoinUri(urlRoot, "/user/get-profile");
         }
-        else
+        else 
         {
+          pluginConfiguration.LookupStringValue(urlTokenValidation, WEB_SERVICE_TOKEN_VALIDATION);
+          if (urlTokenValidation.empty())
+          {
+            pluginConfiguration.LookupStringValue(urlTokenValidation, WEB_SERVICE_TOKEN_VALIDATION_LEGACY);
+          }
+
+          pluginConfiguration.LookupStringValue(urlTokenCreationBase, WEB_SERVICE_TOKEN_CREATION_BASE);
+          pluginConfiguration.LookupStringValue(urlUserProfile, WEB_SERVICE_USER_PROFILE);
+        }
+
+        if (!urlTokenValidation.empty())
+        {
+          LOG(WARNING) << "Authorization plugin: url defined for Token Validation: " << urlTokenValidation;
           authorizationParser_.reset
             (new OrthancPlugins::DefaultAuthorizationParser(factory, dicomWebRoot));
         }
-
-        static const char* WEB_SERVICE_USER_PROFILE = "WebServiceUserProfileUrl";
-        static const char* PERMISSIONS = "Permissions";        
-        if (!pluginConfiguration.LookupStringValue(url, WEB_SERVICE_USER_PROFILE))
-        {
-          LOG(WARNING) << "Authorization plugin: no \"" << WEB_SERVICE_USER_PROFILE << "\" configuration provided.  Will not perform user-permissions based authorization.";
-        }
         else
         {
+          LOG(WARNING) << "Authorization plugin: no url defined for Token Validation";
+        }
+
+        if (!urlUserProfile.empty())
+        {
+          LOG(WARNING) << "Authorization plugin: url defined for User Profile: " << urlUserProfile;
+          
+          static const char* PERMISSIONS = "Permissions";        
           if (!pluginConfiguration.GetJson().isMember(PERMISSIONS))
           {
             throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing required \"" + std::string(PERMISSIONS) + 
-              "\" option since you have defined the \"" + std::string(WEB_SERVICE_USER_PROFILE) + "\" option");
+              "\" option since you have defined the \"" + std::string(WEB_SERVICE_ROOT) + "\" option");
           }
           permissionParser_.reset
             (new OrthancPlugins::PermissionParser(dicomWebRoot, oe2Root));
 
           permissionParser_->Add(pluginConfiguration.GetJson()[PERMISSIONS]);
         }
+        else
+        {
+          LOG(WARNING) << "Authorization plugin: no url defined for User Profile";
+        }
+
+        if (!urlTokenCreationBase.empty())
+        {
+          LOG(WARNING) << "Authorization plugin: base url defined for Token Creation : " << urlTokenCreationBase;
+          // TODO Token Creation
+        }
+        else
+        {
+          LOG(WARNING) << "Authorization plugin: no base url defined for Token Creation";
+        }
 
         if (authorizationParser_.get() == NULL && permissionParser_.get() == NULL)
         {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: Missing one of the mandatory option \"" + std::string(WEB_SERVICE) +
-            "\" or \"" + std::string(WEB_SERVICE_USER_PROFILE) + "\"");
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Authorization plugin: No Token Validation or User Profile url defined");
         }
 
         std::set<std::string> standardConfigurations;
@@ -597,7 +735,9 @@
           }
         }
 
-        std::unique_ptr<OrthancPlugins::AuthorizationWebService> webService(new OrthancPlugins::AuthorizationWebService(url));
+        std::unique_ptr<OrthancPlugins::AuthorizationWebService> webService(new OrthancPlugins::AuthorizationWebService(urlTokenValidation,
+                                                                                                                        urlTokenCreationBase,
+                                                                                                                        urlUserProfile));
 
         std::string webServiceIdentifier;
         if (pluginConfiguration.LookupStringValue(webServiceIdentifier, "WebServiceIdentifier"))
@@ -612,18 +752,24 @@
           webService->SetCredentials(webServiceUsername, webServicePassword);
         }
 
-        std::string webServiceUserProfileUrl;
-        if (pluginConfiguration.LookupStringValue(webServiceUserProfileUrl, "WebServiceUserProfileUrl"))
-        {
-          webService->SetUserProfileUrl(webServiceUserProfileUrl);
-        }
-
         authorizationService_.reset
           (new OrthancPlugins::CachedAuthorizationService
            (webService.release(), factory));
 
-        OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
-        OrthancPlugins::RegisterRestCallback<GetUserProfile>("/auth/user-profile", true);
+        if (!urlTokenValidation.empty())
+        {
+          OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+        }
+        
+        if (!urlUserProfile.empty())
+        {
+          OrthancPlugins::RegisterRestCallback<GetUserProfile>("/auth/user/profile", true);
+        }
+
+        if (!urlTokenCreationBase.empty())
+        {
+          OrthancPlugins::RegisterRestCallback<CreateToken>("/auth/tokens/(.*)", true);
+        }
         
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 2, 1)
         OrthancPluginRegisterIncomingHttpRequestFilter2(context, FilterHttpRequests);
--- a/README	Wed Feb 22 13:13:38 2023 +0100
+++ b/README	Fri Feb 24 18:13:36 2023 +0100
@@ -24,6 +24,49 @@
 http://book.orthanc-server.com/plugins/authorization.html
 
 
+API
+---
+
+Since version 0.5.0, the plugin implements a RestA API to generate tokens
+(provided that the Web service is able to do so).
+
+Sample Orthanc Flavored API:
+
+curl -X PUT http://localhost:8042/auth/tokens/resource-instant-link -H 'Content-Type: application/json' \
+  -d '{"ID": "toto",
+       "Resources" : [{
+         "DicomUid": "1.2",
+         "OrthancId": "",
+         "Level": "study"
+       }],
+       "Type": "resource-instant-link", 
+       "ExpirationDate": "2026-12-31T11:00:00Z"}'
+
+Sample response:
+  {
+    "Token": "e148.....",
+    "Url": null
+  }
+
+The API that must be implemented by the webservice is slighlty different wrt naming conventions:
+
+curl -X PUT http://localhost:8000/tokens/resource-instant-link -H 'Content-Type: application/json' \
+  -d '{"id": "toto",
+       "resources" : [{
+         "dicom-uid": "1.2",
+         "level": "study"
+       }],
+       "type": "resource-instant-link", 
+       "expiration-date": "2026-12-31T11:00:00Z"}'
+
+Sample response:
+  {
+    "token": "e148.....",
+    "url": null
+  }
+
+
+
 Licensing
 ---------