02.05.2025
Dmitrij Purynzin

Why Secure Authentication Matters in Progress OpenEdge?

Enterprise-grade applications require more than just functionality – they demand robust user authentication to safeguard access and ensure data integrity.

This document provides a walkthrough in implementing a secure login and registration system using Progress OpenEdge. Get to know practical techniques like environment-based configuration, hashed passwords, and JWT tokens.

Project objectives

The following objectives define the core components implemented to build a secure and scalable user authentication system in a Progress OpenEdge application.

  • Secure password hashing using SHA-256 with salts retrieved from environment variables.
  • JWT-based stateless session authentication for scalable and secure user management.
  • Separation of sensitive configuration data through environment variable access.
  • Practical application of third-party libraries to extend OpenEdge capabilities (e.g., JWT handling).
  • Robust error handling and clean HTTP response management tailored for API usage.

Progress OpenEdge module's key elements

To meet modern security standards, a full-featured authentication module built in Progress OpenEdge was designed to securely manage user registration and login.

The implementation incorporates the following key elements:

  • Password hashing and salting using MESSAGE-DIGEST and GENERATE-PBE-SALT.
  • Environment variables via OS-GETENV() for secure configuration handling (.env).
  • JWT tokens for stateless authentication, implemented with a third-party library.
  • Secure session management by attaching JWTs to HTTP-only and secure cookies.
  • Modular and extensible code for integration into broader business logic or APIs.

Why JWT?

JSON Web Tokens provide a stateless authentication mechanism perfect for distributed systems. Key benefits are:

  • Self-contained. Contains user claims (username, role, etc.), eliminating DB lookups.
  • Tamper-proof. Digitally signed using HS256 algorithm.
  • Expiration. Built-in token expiry.
  • Cross-service compatibility. Tokens can be verified across services without requiring shared session storage.

Key security features are:

  • Salts and JWT security key. Salt and JWT security key are securely stored in .env.
  • SHA-256 hashing. Industry-standard cryptographic hashing.
  • Secure storage. Only hashes stored, never plaintext passwords.

Learn more about JWT.

Processes in diagrams

Diagrams illustrate the main objectives and flow of the User Registration/Login feature.

Registration process

Login process

Sequence of actions

  • During registration, the system collects user information, including a password. The password is hashed using a secure hashing function (SHA-256 in this case) and a salt from environment variables. The hashed password and other user details are stored in the database.
  • When a user tries to log in, the input password is hashed with the stored salt and compared to the saved hash in the database. This ensures the password remains secure and prevents plaintext storage.
  • Upon successful login, a JWT (JSON Web Token) is generated for the user and stored in a Cookie. The JWT includes claims such as username, email and role, and serves as a session token. The JWT can then be validated by any service that requires authentication, making user verification easy and secure.

Implementation challenges in Progress OpenEdge authentication

Building a secure authentication system in Progress OpenEdge presents several technical and practical challenges, including cryptographic implementation, library limitations, and error handling.

  • Password security. Implementing secure password hashing requires attention to both hash strength and salt usage. It is essential to ensure the hashing and salt generation functions are working correctly.
  • JWT management. Generating and managing JWTs in Progress ABL can be complex. Progress OpenEdge does not provide a built-in feature for JWT generation, so a third-party library is used to implement it. Link is provided in the References section.
  • Error handling. When authentication fails (incorrect password or expired token), the system must handle errors gracefully and provide helpful messages.
  • Limited resources. The relative scarcity of readily available resources, libraries, and best practices for implementing modern authentication and authorisation in Progress ABL can increase development time and complexity. Developers often need to build custom solutions from scratch, which can lead to inconsistencies and potential security vulnerabilities.

Impact and benefits of the authentication implementation

Implementing this feature enhances both the security and usability of the authentication flow in the Progress OpenEdge application. The main outcomes are:

  • Secure authentication. Passwords are securely hashed and stored, reducing the risks of password theft.
  • JWT generation. A third-party library is used to generate JWT tokens upon successful user authentication.
  • Easy session management. JWT tokens streamline user session management and can be easily validated across different parts of the application.

Core security components

1. Password security

Salting & Hashing Flow

User Password → Get Salt From environment variables → SHA-256 Hash → Store (Hash)

PasswordHash  Expand source  

method public character HashPassword(input pcPassword as character): 
  define variable rHash as raw no-undo. 
  define variable rSalt as raw no-undo. 

  /* Retrieve salt from environment variables */ 
  rSalt = hex-decode(os-getenv("SALT")). 
  /* Generate SHA-256 hash with salt */ 
  rHash = message-digest("SHA-256", pcPassword, rSalt). 

  return hex-encode(rHash). 
end method. 

2. Password validation

This method securely verifies a user-suplied password against a stored hash using SHA-256 and a salt retrieved from an environment variable.

MatchPassword  Expand source  

method public logical MatchPassword(input pcUserPassword as character, input pcHashedPassword as character):

method public logical MatchPassword(input pcUserPassword as character, input pcHashedPassword as character): 
  if HashPassword(pcUserPassword) = pcHashedPassword then 
    return true. 
  return false. 
end method. 

3. JWT generation

Token Creation

This method (login) authenticates a user, generates a JSON Web Token (JWT) with user details and a 2-hour expiration, and returns the token which is later set within cookie. It uses a secret key from an environment variable for signing.

login method  Expand source  

using jwtoe.Jwt. 
using jwtoe.JwtBuilder.

method public character login(input pcUsername as character, input pcPassword as character): 
  /* ... Password validation ... */ 

  cJwtToken = Jwt:builder() 
              :setClaim("username", ttAccount.username) 
              :setClaim("role", ttAccount.role) 
              :setExpiresInSeconds(7200)                 // 2h expiry 
              :signWithHS256Key(os-getenv("JWT_SECRET")) // Key from .env 
              :compact(). 

  return cJwtToken. 
end method. 

Using values from JWT payload

This ABL code snippet demonstrates how to parse and validate a JSON Web Token (JWT) using a third-party library. It retrieves claims (data) from the validated token and displays them. It also includes error handling for invalid tokens.

JWT payload  Expand source  

using Progress.Lang.*. 
using jwtoe.Jwt. 
using jwtoe.JwtError.  
using Progress.Json.ObjectModel.JsonObject.  

method public void ValidateJWT( input pcJwtToken as character ): 
def var oClaims as JsonObject no-undo.   

    oClaims = Jwt:parseBuilder() 
                 :setSigningKeyHS256(os-getenv("JWT_SECRET")) // Key from .env 
                 :parseClaimsJws(pcJwtToken). 			     // Token we created in login() method 

    // here you are sure that token was valid 
    message oClaims:GetCharacter("username"). 
    message oClaims:GetCharacter("role"). 

end method. 

4. Environment variable security

It is crucial to recognise that the .env file contains highly sensitive data, including API keys, database credentials, and cryptographic secrets.

Under no circumstances should this file be uploaded to any online repository or shared publicly.

Sample .env

Environment variables  Expand source  

# .env 
# Example how content of .env looks in this case  
SALT=DEADBEEF12345678 
JWT_SECRET=my_super_secret_key_here 

Retrieving Environment Variables in ABL

OS-GETENV()  Expand source  

define variable cSalt   as character no-undo. 
define variable cSecret as character no-undo. 

cSalt = hex-decode(os-getenv("SALT")).  
cSecret = os-getenv("JWT_SECRET"). 

Key code snippets

1. Format Response

This ABL method (FormatResponse) streamlines HTTP response creation: it builds a JSON response with a given status code and body, optionally adds a JWT cookie, and writes the response, reducing boilerplate code.

Important to mention:

  • HttpOnly: This prevents client-side JavaScript from accessing the cookie. This mitigates the risk of cross-site scripting attacks, where malicious scripts could steal the cookie and compromise the user's session.
  • SecureOnly: This ensures the cookie is only sent over HTTPS connections. This prevents attackers from intercepting the cookie and the JWT it contains.

FormatResponse()  Expand source  

 method protected void FormatResponse(input poStatusEnum as StatusCodeEnum, input pcBody as character):

  method protected void FormatResponse(input poStatusEnum as StatusCodeEnum, input pcBody as character): 
    define variable oWriter   as WebResponseWriter no-undo. 
    define variable oResponse as WebResponse       no-undo. 
    define variable oCookie   as Cookie            no-undo. 
    define variable dtExpires as datetime-tz       no-undo. 

    dtExpires = now + 3600. 

    assign 
    	oResponse             = new WebResponse() 
    	oResponse:StatusCode  = integer(poStatusEnum) 
    	oResponse:Entity      = new String(pcBody) 
    	oResponse:ContentType = "application/json" 
    	. 

    if this-object:cJwtToken <> "" then  
    do:            
oCookie = new Cookie( 
        	"token",               // CookieName  
        	"",                    // Domain (empty string if not needed) 
        	"/",                   // Path  
        	this-object:cJwtToken, // CookieValue  
        	3600,                  // MaxAge in seconds 
        	dtExpires,             // ExpiresAt: one hour from now  
        	true,                  // SecureOnly: true for HTTPS  
        	true,                  // HttpOnly: true for security  
        	1.0                    // Version: typically 1.0  
      	). 

oResponse:SetCookie(oCookie). 
    	this-object:cJwtToken = "". 
    end. 

    oWriter = new WebResponseWriter(oResponse). 
    oWriter:Open(). 
    oWriter:Close(). 
  end method. 

2. Registration handler

This ABL method handles user registration: it parses JSON, creates an account, and returns an HTTP response (201 for success, 400 or 500 for errors).

HandleRegister  Expand source  

method private integer HandleRegister(poRequest as IWebRequest): 
    define variable hAccount      as handle     no-undo. 
    define variable oJsonBody     as JsonObject no-undo. 
    define variable oAccountArray as JsonArray  no-undo. 

    empty temp-table ttAccount. 

    oJsonBody     = cast(poRequest:Entity, JsonObject). 
    hAccount      = temp-table ttAccount:handle. 
    oAccountArray = cast(oJsonBody:GetJsonArray("ttAccount"), JsonArray).  
    hAccount:read-json("JsonArray", oAccountArray, "append"). 

    oAccountService:accountForm(table ttAccount). 
// Successful response 
    this-object:FormatResponse(StatusCodeEnum:Created,"Account Created"). 
    // Responses on errors 
    catch e as Progress.Lang.Error: 
      if e:GetClass() = get-class(AppError) then 
        this-object:FormatResponse(StatusCodeEnum:BadRequest,"Registration Failed: " + e:GetMessage(1)). 
      else 
        this-object:FormatResponse(StatusCodeEnum:InternalServerError,"Server Error"). 
    end catch. 
end method. 

Imitating registration front-end with Postman:

  • The client sends a POST request to the /register endpoint/
  • The request body contains a JSON array named ttAcount with a single JsonObject representing user data.

It is important to note that plain text passwords are being sent in this request. Therefore, HTTPS must be used to encrypt data and prevent credential exposure during transit.

No email provided error handling ex.:

3. Login handler

This ABL method handles user login: it parses JSON credentials, authenticates, generates a JWT, and returns an HTTP response (200 with token, 401 or 500 on errors).

HandleLogin  Expand source  

method private integer HandleLogin(poRequest as IWebRequest): 
    define variable cUsername     as character         no-undo. 
    define variable cPassword     as character         no-undo. 
    define variable oJsonBody     as JsonObject        no-undo. 
    define variable oAccountArray as JsonArray         no-undo. 

    empty temp-table ttAccount.   

    oJsonBody     = cast(poRequest:Entity, JsonObject). 
    oAccountArray = cast(oJsonBody:GetJsonArray("ttAccount"), JsonArray).  
    oJsonBody     = cast(oAccountArray:GetJsonObject(1), JsonObject). 
    cUsername     = oJsonBody:GetCharacter("username"). 
    cPassword     = oJsonBody:GetCharacter("password").  

    this-object:cJwtToken = oAccountService:login(cUsername, cPassword). 
// Successful response 
    this-object:FormatResponse(StatusCodeEnum:OK, "Login Successful"). 
// Responses on errors 
    catch e as Progress.Lang.Error: 
      this-object:cJwtToken = "". 
 	  if e:GetClass() = get-class(AppError) then 
        this-object:FormatResponse(StatusCodeEnum:Unauthorized,"Login Failed: " + e:GetMessage(1)). 
      else 
        this-object:FormatResponse(StatusCodeEnum:InternalServerError,"Server Error"). 
    end catch. 
end method. 

Imitating login Front-end with Postman:

  • The client sends a POST request to the /login endpoint.
  • The request body contains a JSON array named ttAccount with a single JsonObject representing the user's login credentials (username and password).

It is important to note that plain text passwords are being sent in this request. Therefore, HTTPS must be used to encrypt the data and prevent credential exposure during transit.

User not found error handling ex.:

4. Create Account

This method creates new Account records from a ttAccount temp-table, assigning a unique ID and transferring data.

createAccount  Expand source  

   method public void createAccount(input table ttAccount): 
    define buffer bAccount for Account.      

    find first ttAccount. 
    if this-object:checkAccountAlreadyExists(ttAccount.userName) then  
      undo, throw new AppError('Username already exists', 1).  

    create bAccount. 
    assign 
      bAccount.id       = next-value(AccountId) 
      bAccount.username = ttAccount.username 
      bAccount.email    = ttAccount.email 
      bAccount.password = ttAccount.password 
      bAccount.role     = ttAccount.role 
      . 
    release bAccount. 
  end method. 

JWT authorisation and access control

This utility class handles JWT-based authentication.

It reads the token from the request cookie, verifies its signature and expiration using the JwtParser class, and returns the decoded user claims.

JwtGuard  Expand source  

using jwtoe.JwtParser. 
using jwtoe.JwtError. 
using Progress.Json.ObjectModel.JsonObject. 
using OpenEdge.Net.HTTP.Cookie. 
using OpenEdge.Web.IWebRequest. 
using Progress.Lang.*.  

class JwtGuard: 

  /* This method checks if the token is valid and returns the claims */ 
  method public static JsonObject RequireValidTokenFromCookie(poRequest as IWebRequest): 
    define variable oCookie    as Cookie     no-undo. 
    define variable cToken     as character  no-undo. 
    define variable oClaims    as JsonObject no-undo. 
    define variable oParser    as JwtParser  no-undo.  

    /* Get cookie */ 
    oCookie = poRequest:GetCookie("token"). 
    if not valid-object(oCookie) or oCookie:Value = "" then 
      undo, throw new AppError("Missing cookie. You are not logged in", 401). 

    cToken = oCookie:Value. 
    /* Parse the JWT token */ 
    oParser = new JwtParser(). 
    oParser:setSigningKeyHS256(os-getenv("JWT_SECRET")). 
    oClaims = oParser:parseClaimsJws(cToken). 

    return oClaims. 
  end method. 
end class. 

If the token is missing, invalid, or expired, it throws an AppError, which results in an Unauthorized (401) response. This effectively determines whether a user is logged in.

JwtValidation  Expand source  

define variable oClaims as JsonObject no-undo. 

oClaims = JwtGuard:RequireValidTokenFromCookie(poRequest). 

Once the claims are returned, role-based access control can easily be applied. For example, to allow access only for admins:

Role validation  Expand source  

if oClaims:GetCharacter("role") <> "admin" then 
do: 
  this-object:FormatResponse(StatusCodeEnum:Forbidden, "Access Denied: Only admin can view all accounts"). 
  return 0. 
end. 

This makes JwtGuard a clean and centralized way to both authenticate users and enforce role permissions across protected endpoints.

To log out we simply clear out the Cookie by setting its expiration to zero:

Log out  Expand source  

method protected integer HandleLogout(poRequest as IWebRequest): 
  define variable oResponse as IHttpResponse     no-undo. 
  define variable oWriter   as WebResponseWriter no-undo. 
  define variable oCookie   as Cookie            no-undo. 
  define variable dtExpired as datetime-tz       no-undo. 

  dtExpired = now - 1. /* Expired timestamp */    

  oCookie = new Cookie( 
    "token", "", "/", "", 0, dtExpired, false, true, 1.0 
  ). 

  oResponse = new WebResponse(). 
  oResponse:SetCookie(oCookie). 
  oResponse:StatusCode  = int(StatusCodeEnum:OK). 
  oResponse:Entity      = new OpenEdge.Core.String("Logged out"). 
  oResponse:ContentType = "application/json".    

  oWriter = new WebResponseWriter(oResponse). 
  oWriter:Open(). 
  oWriter:Close(). 

  return 0. 
end method. 

Authentication flow in action

1. Logging in as a regular user

The user logs in with valid credentials but has a non-admin role (e.g., "role": "user"). A JWT token is returned and saved in a cookie.

2. Attempting to access /account as a regular user

This protected endpoint is restricted to admin users. When the non-admin user tries to access it, the server checks the JWT claims and responds with:

3. Logging in as an admin

The user logs in again, this time with admin credentials. A new JWT token is issued and stored in the cookie.

4. Accessing /account as an admin

This time the JWT contains the correct admin role. The request is allowed and the server responds with a list of accounts.

5. Logging out

The user sends a request to the /logout endpoint, which clears the cookie by setting it with an expired timestamp.

6. Attempting to access /account after logout

Since the JWT is now expired or missing, the next request to /account is rejected.

Need help implementing secure authentication in Progress OpenEdge?

The Baltic Amadeus Progress OpenEdge team is here to support you – whether you are planning a new architecture, modernising legacy logic, or integrating authentication with external systems. Contact us today to get expert guidance tailored to your goals.

Project example:

GitLab repository for this project

 

References:

Third-party library for JWT OE  

Progress OpenEdge Hashing Function  

Progress OpenEdge OS-GETENV() Function  

Progress OpenEdge Salt Function  

Progress OpenEdge Generating encryption keys  

Postman extension for VS Code

Let’s talk about your project

Starting something new or need support for an existing project? Reach out, and our experts will get back to you within one business day.

Start the conversation

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.