-----------------------------------------------------------------------
-- security-oauth-servers -- OAuth Server Authentication Support
-- Copyright (C) 2016, 2017, 2018 Stephane Carrez
-- Written by Stephane Carrez (Stephane.Carrez@gmail.com)
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
-----------------------------------------------------------------------
with Ada.Calendar.Conversions;
with Interfaces.C;
with Util.Log.Loggers;
with Util.Encoders.Base64;
with Util.Encoders.SHA256;
with Util.Encoders.HMAC.SHA256;
package body Security.OAuth.Servers is
use type Ada.Calendar.Time;
Log : constant Util.Log.Loggers.Logger := Util.Log.Loggers.Create ("Security.OAuth.Servers");
-- ------------------------------
-- Check if the application has the given permission.
-- ------------------------------
function Has_Permission (App : in Application;
Permission : in Permissions.Permission_Index) return Boolean is
begin
return Security.Permissions.Has_Permission (App.Permissions, Permission);
end Has_Permission;
protected body Token_Cache is
procedure Authenticate (Token : in String;
Grant : in out Grant_Type) is
Pos : Cache_Map.Cursor := Entries.Find (Token);
begin
if Cache_Map.Has_Element (Pos) then
if Grant.Expires < Ada.Calendar.Clock then
Entries.Delete (Pos);
Grant.Status := Expired_Grant;
else
Grant.Auth := Cache_Map.Element (Pos).Auth;
Grant.Expires := Cache_Map.Element (Pos).Expire;
Grant.Status := Valid_Grant;
end if;
end if;
end Authenticate;
procedure Insert (Token : in String;
Expire : in Ada.Calendar.Time;
Principal : in Principal_Access) is
begin
Entries.Insert (Token, Cache_Entry '(Expire, Principal));
end Insert;
procedure Remove (Token : in String) is
begin
Entries.Delete (Token);
end Remove;
procedure Timeout is
begin
null;
end Timeout;
end Token_Cache;
-- ------------------------------
-- Set the auth private key.
-- ------------------------------
procedure Set_Private_Key (Manager : in out Auth_Manager;
Key : in String;
Decode : in Boolean := False) is
begin
if Decode then
declare
Decoder : constant Util.Encoders.Decoder
:= Util.Encoders.Create (Util.Encoders.BASE_64_URL);
Content : constant String := Decoder.Decode (Key);
begin
Manager.Private_Key := To_Unbounded_String (Content);
end;
else
Manager.Private_Key := To_Unbounded_String (Key);
end if;
end Set_Private_Key;
-- ------------------------------
-- Set the application manager to use and and applications.
-- ------------------------------
procedure Set_Application_Manager (Manager : in out Auth_Manager;
Repository : in Application_Manager_Access) is
begin
Manager.Repository := Repository;
end Set_Application_Manager;
-- ------------------------------
-- Set the realm manager to authentify users.
-- ------------------------------
procedure Set_Realm_Manager (Manager : in out Auth_Manager;
Realm : in Realm_Manager_Access) is
begin
Manager.Realm := Realm;
end Set_Realm_Manager;
-- ------------------------------
-- Authorize the access to the protected resource by the application and for the
-- given principal. The resource owner has been verified and is represented by the
-- Auth principal. Extract from the request parameters represented by
-- Params the application client id, the scope and the expected response type.
-- Handle the "Authorization Code Grant" and "Implicit Grant" defined in RFC 6749.
-- ------------------------------
procedure Authorize (Realm : in out Auth_Manager;
Params : in Security.Auth.Parameters'Class;
Auth : in Principal_Access;
Grant : out Grant_Type) is
Method : constant String := Params.Get_Parameter (Security.OAuth.RESPONSE_TYPE);
Client_Id : constant String := Params.Get_Parameter (Security.OAuth.CLIENT_ID);
begin
if Client_Id'Length = 0 then
Grant.Status := Invalid_Grant;
Grant.Error := INVALID_REQUEST'Access;
return;
end if;
declare
App : constant Application'Class := Realm.Repository.Find_Application (Client_Id);
begin
if Method = "code" then
Realm.Authorize_Code (App, Params, Auth, Grant);
elsif Method = "token" then
Realm.Authorize_Token (App, Params, Auth, Grant);
else
Grant.Status := Invalid_Grant;
Grant.Error := UNSUPPORTED_RESPONSE_TYPE'Access;
Log.Warn ("Authorize method '{0}' is not supported", Method);
end if;
end;
exception
when Invalid_Application =>
Log.Warn ("Invalid client_id {0}", Client_Id);
Grant.Status := Invalid_Grant;
Grant.Error := INVALID_CLIENT'Access;
return;
when E : others =>
Log.Error ("Error while doing authorization for client_id " & Client_Id, E);
Grant.Status := Invalid_Grant;
Grant.Error := SERVER_ERROR'Access;
end Authorize;
-- ------------------------------
-- The Token procedure is the main entry point to get the access token and
-- refresh token. The request parameters are accessed through the Params interface.
-- The operation looks at the "grant_type" parameter to identify the access method.
-- It also looks at the "client_id" to find the application for which the access token
-- is created. Upon successful authentication, the operation returns a grant.
-- ------------------------------
procedure Token (Realm : in out Auth_Manager;
Params : in Security.Auth.Parameters'Class;
Grant : out Grant_Type) is
Method : constant String := Params.Get_Parameter (Security.OAuth.GRANT_TYPE);
Client_Id : constant String := Params.Get_Parameter (Security.OAuth.CLIENT_ID);
begin
if Length (Realm.Private_Key) < MIN_KEY_LENGTH then
Log.Error ("The private key is too short to generate a secure token");
Grant.Status := Invalid_Grant;
Grant.Error := SERVER_ERROR'Access;
return;
end if;
if Client_Id'Length = 0 then
Grant.Status := Invalid_Grant;
Grant.Error := INVALID_REQUEST'Access;
return;
end if;
declare
App : constant Application'Class := Realm.Repository.Find_Application (Client_Id);
begin
if Method = "authorization_code" then
Realm.Token_From_Code (App, Params, Grant);
elsif Method = "password" then
Realm.Token_From_Password (App, Params, Grant);
elsif Method = "refresh_token" then
Grant.Error := UNSUPPORTED_GRANT_TYPE'Access;
elsif Method = "client_credentials" then
Grant.Error := UNSUPPORTED_GRANT_TYPE'Access;
else
Grant.Error := UNSUPPORTED_GRANT_TYPE'Access;
Log.Warn ("Grant type '{0}' is not supported", Method);
end if;
end;
exception
when Invalid_Application =>
Log.Warn ("Invalid client_id '{0}'", Client_Id);
Grant.Status := Invalid_Grant;
Grant.Error := INVALID_CLIENT'Access;
return;
end Token;
-- ------------------------------
-- Format the expiration date to a compact string. The date is transformed to a Unix
-- date and encoded in LEB128 + base64url.
-- ------------------------------
function Format_Expire (Expire : in Ada.Calendar.Time) return String is
T : constant Interfaces.C.long := Ada.Calendar.Conversions.To_Unix_Time (Expire);
begin
return Util.Encoders.Base64.Encode (Interfaces.Unsigned_64 (T));
end Format_Expire;
-- ------------------------------
-- Decode the expiration date that was extracted from the token.
-- ------------------------------
function Parse_Expire (Expire : in String) return Ada.Calendar.Time is
V : constant Interfaces.Unsigned_64 := Util.Encoders.Base64.Decode (Expire);
begin
return Ada.Calendar.Conversions.To_Ada_Time (Interfaces.C.long (V));
end Parse_Expire;
-- Implement the RFC 6749: 4.1.1. Authorization Request for the authorization code grant.
procedure Authorize_Code (Realm : in out Auth_Manager;
App : in Application'Class;
Params : in Security.Auth.Parameters'Class;
Auth : in Principal_Access;
Grant : out Grant_Type) is
Callback : constant String := Params.Get_Parameter (Security.OAuth.REDIRECT_URI);
Scope : constant String := Params.Get_Parameter (Security.OAuth.SCOPE);
begin
Grant.Request := Code_Grant;
Grant.Status := Invalid_Grant;
if Auth = null then
Log.Info ("Authorization is denied");
Grant.Error := ACCESS_DENIED'Access;
elsif App.Callback /= Callback then
Log.Info ("Invalid application callback");
Grant.Error := UNAUTHORIZED_CLIENT'Access;
else
-- Manager'Class (Realm).Authorize (Auth, Scope);
Grant.Expires := Ada.Calendar.Clock + Realm.Expire_Code;
Grant.Expires_In := Realm.Expire_Code;
Grant.Status := Valid_Grant;
Grant.Auth := Auth;
Realm.Create_Token (Realm.Realm.Authorize (App, Scope, Auth), Grant);
end if;
end Authorize_Code;
-- Implement the RFC 6749: 4.2.1. Authorization Request for the implicit grant.
procedure Authorize_Token (Realm : in out Auth_Manager;
App : in Application'Class;
Params : in Security.Auth.Parameters'Class;
Auth : in Principal_Access;
Grant : out Grant_Type) is
Callback : constant String := Params.Get_Parameter (Security.OAuth.REDIRECT_URI);
Scope : constant String := Params.Get_Parameter (Security.OAuth.SCOPE);
begin
Grant.Request := Implicit_Grant;
Grant.Status := Invalid_Grant;
if Auth = null then
Log.Info ("Authorization is denied");
Grant.Error := ACCESS_DENIED'Access;
elsif App.Callback /= Callback then
Log.Info ("Invalid application callback");
Grant.Error := UNAUTHORIZED_CLIENT'Access;
else
Grant.Expires := Ada.Calendar.Clock + App.Expire_Timeout;
Grant.Expires_In := App.Expire_Timeout;
Grant.Status := Valid_Grant;
Grant.Auth := Auth;
Realm.Create_Token (Realm.Realm.Authorize (App, Scope, Grant.Auth), Grant);
end if;
end Authorize_Token;
-- Make the access token from the authorization code that was created by the
-- Authorize operation. Verify the client application, the redirect uri, the
-- client secret and the validity of the authorization code. Extract from the
-- authorization code the auth principal that was used for the grant and make the
-- access token.
procedure Token_From_Code (Realm : in out Auth_Manager;
App : in Application'Class;
Params : in Security.Auth.Parameters'Class;
Grant : out Grant_Type) is
Code : constant String := Params.Get_Parameter (Security.OAuth.CODE);
Callback : constant String := Params.Get_Parameter (Security.OAuth.REDIRECT_URI);
Secret : constant String := Params.Get_Parameter (Security.OAuth.CLIENT_SECRET);
Token : Token_Validity;
begin
Grant.Request := Code_Grant;
Grant.Status := Invalid_Grant;
if Code'Length = 0 then
Log.Info ("Missing authorization code request parameter");
Grant.Error := INVALID_REQUEST'Access;
elsif App.Secret /= Secret then
Log.Info ("Invalid application secret");
Grant.Error := UNAUTHORIZED_CLIENT'Access;
elsif App.Callback /= Callback then
Log.Info ("Invalid application callback");
Grant.Error := UNAUTHORIZED_CLIENT'Access;
else
Token := Realm.Validate (To_String (App.Client_Id), Code);
Grant.Status := Token.Status;
if Token.Status /= Valid_Grant then
Log.Info ("Invalid authorization code {0}", Code);
Grant.Error := ACCESS_DENIED'Access;
else
-- Verify the identification token and get the principal.
Realm.Realm.Verify (Code (Token.Ident_Start .. Token.Ident_End), Grant.Auth);
if Grant.Auth = null then
Log.Info ("Access denied for authorization code {0}", Code);
Grant.Error := ACCESS_DENIED'Access;
else
-- Extract user/session ident from code.
Grant.Expires := Ada.Calendar.Clock + App.Expire_Timeout;
Grant.Expires_In := App.Expire_Timeout;
Grant.Error := null;
Realm.Create_Token (Realm.Realm.Authorize (App, SCOPE, Grant.Auth), Grant);
end if;
end if;
end if;
end Token_From_Code;
-- ------------------------------
-- Make the access token from the resource owner password credentials. The username,
-- password and scope are extracted from the request and they are verified through the
-- Verify procedure to obtain an associated principal. When successful, the
-- principal describes the authorization and it is used to forge the access token.
-- This operation implements the RFC 6749: 4.3. Resource Owner Password Credentials Grant.
-- ------------------------------
procedure Token_From_Password (Realm : in out Auth_Manager;
App : in Application'Class;
Params : in Security.Auth.Parameters'Class;
Grant : out Grant_Type) is
Username : constant String := Params.Get_Parameter (Security.OAuth.USERNAME);
Password : constant String := Params.Get_Parameter (Security.OAuth.PASSWORD);
Scope : constant String := Params.Get_Parameter (Security.OAuth.SCOPE);
Secret : constant String := Params.Get_Parameter (Security.OAuth.CLIENT_SECRET);
begin
Grant.Request := Password_Grant;
Grant.Status := Invalid_Grant;
if Username'Length = 0 then
Log.Info ("Missing username request parameter");
Grant.Error := INVALID_REQUEST'Access;
elsif Password'Length = 0 then
Log.Info ("Missing password request parameter");
Grant.Error := INVALID_REQUEST'Access;
elsif App.Secret /= Secret then
Log.Info ("Invalid application secret");
Grant.Error := UNAUTHORIZED_CLIENT'Access;
else
-- Verify the username and password to get the principal.
Realm.Realm.Verify (Username, Password, Grant.Auth);
if Grant.Auth = null then
Log.Info ("Access denied for {0}", Username);
Grant.Error := ACCESS_DENIED'Access;
else
Grant.Status := Valid_Grant;
Grant.Expires := Ada.Calendar.Clock + App.Expire_Timeout;
Grant.Expires_In := App.Expire_Timeout;
Grant.Error := null;
Realm.Create_Token (Realm.Realm.Authorize (App, Scope, Grant.Auth), Grant);
end if;
end if;
end Token_From_Password;
-- ------------------------------
-- Create a HMAC-SHA1 of the data with the private key.
-- This function can be overriden to use another signature algorithm.
-- ------------------------------
function Sign (Realm : in Auth_Manager;
Data : in String) return String is
Ctx : Util.Encoders.HMAC.SHA256.Context;
Result : Util.Encoders.SHA256.Base64_Digest;
begin
Util.Encoders.HMAC.SHA256.Set_Key (Ctx, To_String (Realm.Private_Key));
Util.Encoders.HMAC.SHA256.Update (Ctx, Data);
Util.Encoders.HMAC.SHA256.Finish_Base64 (Ctx, Result, True);
return Result;
end Sign;
-- ------------------------------
-- Forge an access token. The access token is signed by an HMAC-SHA256 signature.
-- The returned token is formed as follows:
-- ..HMAC-SHA256(, .)
-- See also RFC 6749: 5. Issuing an Access Token
-- ------------------------------
procedure Create_Token (Realm : in Auth_Manager;
Ident : in String;
Grant : in out Grant_Type) is
Exp : constant String := Format_Expire (Grant.Expires);
Data : constant String := Exp & "." & Ident;
Hmac : constant String := Auth_Manager'Class (Realm).Sign (Data);
begin
Grant.Token := Ada.Strings.Unbounded.To_Unbounded_String (Data & "." & Hmac);
end Create_Token;
-- Validate the token by checking that it is well formed, it has not expired
-- and the HMAC-SHA256 signature is valid. Return the set of information to allow
-- the extraction of the auth identification from the token public part.
function Validate (Realm : in Auth_Manager;
Client_Id : in String;
Token : in String) return Token_Validity is
Pos1 : constant Natural := Util.Strings.Index (Token, '.');
Pos2 : constant Natural := Util.Strings.Rindex (Token, '.');
Result : Token_Validity := (Status => Invalid_Grant, others => <>);
begin
-- Verify the access token validity.
if Pos1 = 0 or Pos2 = 0 or Pos1 = Pos2 then
Log.Info ("Authenticate bad formed access token {0}", Token);
return Result;
end if;
-- Build the HMAC signature with the private key.
declare
Hmac : constant String
:= Auth_Manager'Class (Realm).Sign (Token (Token'First .. Pos2 - 1));
begin
-- Check the HMAC signature part.
if Token (Pos2 + 1 .. Token'Last) /= Hmac then
Log.Info ("Bad signature for access token {0}", Token);
return Result;
end if;
-- Signature is valid we can check the token expiration date.
Result.Expire := Parse_Expire (Token (Token'First .. Pos1 - 1));
if Result.Expire < Ada.Calendar.Clock then
Log.Info ("Token {0} has expired", Token);
Result.Status := Expired_Grant;
return Result;
end if;
Result.Ident_Start := Pos1 + 1;
Result.Ident_End := Pos2 - 1;
-- When an identifier is passed, verify it.
if Client_Id'Length > 0 then
Result.Ident_Start := Util.Strings.Index (Token, '.', Pos1 + 1);
if Result.Ident_Start = 0
or else Client_Id /= Token (Pos1 + 1 .. Result.Ident_Start - 1)
then
Log.Info ("Token {0} was stealed for another application", Token);
Result.Status := Stealed_Grant;
return Result;
end if;
end if;
-- The access token is valid.
Result.Status := Valid_Grant;
return Result;
end;
exception
when E : others =>
-- No exception should ever be raised because we verify the signature first.
Log.Error ("Token " & Token & " raised an exception", E);
Result.Status := Invalid_Grant;
return Result;
end Validate;
-- ------------------------------
-- Authenticate the access token and get a security principal that identifies the app/user.
-- See RFC 6749, 7. Accessing Protected Resources.
-- The access token is first searched in the cache. If it was found, it means the access
-- token was already verified in the past, it is granted and associated with a principal.
-- Otherwise, we have to verify the token signature first, then the expiration date and
-- we extract from the token public part the auth identification. The Authenticate
-- operation is then called to obtain the principal from the auth identification.
-- When access token is invalid or authentification cannot be verified, a null principal
-- is returned. The Grant data will hold the result of the grant with the reason
-- of failures (if any).
-- ------------------------------
procedure Authenticate (Realm : in out Auth_Manager;
Token : in String;
Grant : out Grant_Type) is
Cacheable : Boolean;
Check : Token_Validity;
begin
Check := Realm.Validate ("", Token);
Grant.Status := Check.Status;
Grant.Request := Access_Grant;
Grant.Expires := Check.Expire;
Grant.Auth := null;
if Check.Status = Expired_Grant then
Log.Info ("Access token {0} has expired", Token);
elsif Check.Status /= Valid_Grant then
Log.Info ("Access token {0} is invalid", Token);
else
Realm.Cache.Authenticate (Token, Grant);
if Grant.Auth /= null then
Log.Debug ("Authenticate access token {0} succeeded from cache", Token);
return;
end if;
-- The access token is valid, well formed and has not expired.
-- Get the associated principal (the only possibility it could fail is
-- that it was revoked).
Realm.Realm.Authenticate (Token (Check.Ident_Start .. Check.Ident_End),
Grant.Auth, Cacheable);
if Grant.Auth = null then
Log.Info ("Access token {0} was revoked", Token);
Grant.Status := Revoked_Grant;
-- We are allowed to keep the token in the cache, insert it.
elsif Cacheable then
Realm.Cache.Insert (Token, Check.Expire, Grant.Auth);
Log.Debug ("Access token {0} is granted and inserted in the cache", Token);
else
Log.Debug ("Access token {0} is granted", Token);
end if;
end if;
end Authenticate;
procedure Revoke (Realm : in out Auth_Manager;
Token : in String) is
Check : Token_Validity;
Auth : Principal_Access;
Cacheable : Boolean;
begin
Check := Realm.Validate ("", Token);
if Check.Status = Valid_Grant then
-- The access token is valid, well formed and has not expired.
-- Get the associated principal (the only possibility it could fail is
-- that it was revoked).
Realm.Realm.Authenticate (Token (Check.Ident_Start .. Check.Ident_End),
Auth, Cacheable);
if Auth /= null then
Realm.Cache.Remove (Token);
Realm.Realm.Revoke (Auth);
end if;
end if;
end Revoke;
end Security.OAuth.Servers;