/*****************************************************************************/ /* authagent_example.c Example authentication agent CGIplus script, using the callout mechanism. For some in-built usernames and passwords that can be used for experimentation/demonstration see AuthenticateUser() structure array UserNamePasswordPairs[]. This example can be used in four ways. 1. As a realm authenticator. Here the agent is simply used to authenticate the user password for the realm. ["Authentication Agent test 1"=AUTHAGENT_EXAMPLE=agent] /some/path/or/other/* Load the new authorization and access the path (for username/password details see AuthenticateUser() below). Of course processing can be WATCHed. 2. For determining group membership. ["Authentication Agent test 2"=AUTHAGENT_EXAMPLE=agent;AUTHAGENT_EXAMPLE=agent] /some/path/or/other/* Now group membership must be determined before access is granted (the agent is used twice, first for authentication then for group membership). Note that in MemberOfGroup() only "moe", "larry" and "curly" are group members, "mark" is not and so should be refused access. 3. As a complete authenticator/authorizor. Using the path, query string, remote host, etc., CGI variables, along with authentication-specific ones, the agent can assume the role of complete authenticator with any and all checks and processing desired. To provide per-path information to such an authenticator the "param=" authorization keyword allows any parameter(s) required (they can be placed in single or double quotes if required). Typically this might be the source of the authentication. The third example demonstrates this. ["Authentication Agent test 3"=AUTHAGENT_EXAMPLE=agent] /some/path/or/other/* param=WASD_ROOT:[SRC.CGIPLUS]AUTHAGENT_EXAMPLE.LIS 4. As a non-username/password authenticator Normally an agent uses the WWW_REMOTE_USER and WWW_AUTH_PASSWORD, or the WWW_HTTP_AUTHORIZATION, CGI variables to obtain authorization information supplied in the request header. These are initially requested by the server automatically generating a 401/WWW-Authorize: response when there are none present. Of course it is possible for an agent to perform authorization outside of this mechanism (examples implemented internally by the WASD server include SSL X.509 client certificate, and the RFC1413 identification protocol). To suppress the automatic requesting of a username/password by the server make the first portion of the agent parameter "/NO401". The server detects this and suppresses it's initial 401 response. For example ["Authentication Agent test 4"=AUTHAGENT_EXAMPLE=agent] /some/path/or/other/* param="/NO401 ANY-OTHER-PARAMETER" GENERAL GUIDELINES ------------------ The CGI variable WWW_AUTH_AGENT will *only* exist for authentication agents and may be used to verify the HTTPd invoked the script, not a user. If this variable does not exist the authentication agent should immediately exit. The agent should also check that it is executing within the CGIplus environment and also immediately exit if not. All normal CGI/CGIplus variables are available for use if desired (.e.g WWW_PATH_INFO, WWW_QUERY_STRING, etc.) The server processes records, hence each "line" of output must be fflush()ed so as not to be buffered by the CRTL. The authentication agent may request the server to directly output an error message. The response line must begin "500 ", with any text following the status code being output as the error message. Generally this would not be necessary as the server will generate an appropriate message for authentication or group membership failure. The WATCH facility is a valuable adjunct in understanding/debugging agent script behaviour. Response lines beginning "000 " are ignored and may be used to provide WATCHable debug information (remembering all I/O adds to overhead in production scripts). (Debug) may also be turned on. Although this will prevent the script engaging in a dialog with the server it will output the debug information directly to the browser, which may provide further information when developing/debugging. AUTHENTICATING A USERNAME/PASSWORD ---------------------------------- The transaction details are found in the following CGI variables. WWW_AUTH_AGENT .................. "REALM" or other parameter WWW_AUTH_PASSWORD ............... user supplied case-sensitive password WWW_AUTH_REALM .................. realm name (same as agent name) WWW_AUTH_REALM_DESCRIPTION ...... realm description user is prompted with WWW_REMOTE_USER ................. case-sensitive username The WWW_AUTH_AGENT variable indicates whether authentication ("REALM") or group membership ("GROUP") is being requested. The realm description could be used to differentiate multiple authentication realms to the one script (as WWW_AUTH_REALM is always set to the name of the agent script). Return a response (digits and 'access' are mandatory, other text is optional - although can provide valuable development/debugging information): '000 any text' ........... ignored by the server, provides WATCHable debug info '100 AUTHAGENT-CALLOUT' .. server abort if the agent is being run as a script '100 LIFETIME integer' ... set script's CGIplus lifetime (zero makes infinite) '100 NOCACHE' ............ do not cache the results of this authorization '100 REASON any text' .... reason for the authentication being denied '100 REMOTE-USER name .... provide user name (authenticated some non-401 way) '100 VMS-USER name ....... this username is a VMS username, treat it as such '100 SET-COOKIE cookie' .. RFC2109 cookie (generates "Set-Cookie:" header) '100 USER any text' ...... provide user details (only after 200 response) '200 access' ............. the username/password verified access: "READ", "WRITE", "READ+WRITE", "FULL" '401 reason' ............. username/password did not verify '401 "realm"' ............ did not verify, (quoted) browser prompt string '403 reason' ............. access is forbidden '500 description' ........ script error to be reported via server VMS-USER issues: when a VMS-USER is passed back to the server the username undergoes all VMS authorization processing (e.g. ID, prime days, etc) - except password checking, it is assumed the agent has authenticated the username. The access level (R, R+W, etc.) is derived from the SYSUAF information - unless the agent *subsequently* provides a "200 access" callout. The user details come from the SYSUAF - unless the agent *subsequently* provides a "100 USER details" callout. ESTABLISHING GROUP MEMBERSHIP ----------------------------- The transaction details are found in the following CGI variables. WWW_AUTH_AGENT .................. "GROUP" WWW_AUTH_GROUP .................. name of group WWW_REMOTE_USER ................. case-sensitive username Valid responses (digits are mandatory, other text is optional): '000 any text' ........... ignored by the server, provides WATCHable debug info '100 LIFETIME integer' ... set script's CGIplus lifetime (zero makes infinite) '100 NOCACHE' ............ do not cache the results of this authorization '100 REASON any text' .... reason for the authentication being denied '100 SET-COOKIE cookie' .. RFC2109 cookie (generates "Set-Cookie:" header) '200 any text' ........... indicates group membership '403 reason' ............. indicates not a group member '500 description' ........ script error to be reported via server LOGICAL NAMES ------------- AUTHAGENT_EXAMPLE_NO401 see above AUTHAGENT_EXAMPLE$DBUG turns on all "if (Debug)" statements AUTHAGENT_EXAMPLE$WATCH turns on agent "000 message" WATCH statements Debug statements do not work very well inside authentication agent scripts. Use the WATCH statements using the server WATCH facility to observe script processing (for debug purposes). BUILD DETAILS ------------- Compile then link: $ @BUILD_AUTHAGENT_EXAMPLE To just link: $ @BUILD_AUTHAGENT_EXAMPLE LINK COPYRIGHT --------- Copyright (C) 1999-2021 Mark G.Daniel 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. VERSION HISTORY (update SOFTWAREVN as well) --------------- 10-JUN-2021 MGD v2.0.0, minimal changes for WASD v12... agent requirements 11-MAY-2007 MGD v1.6.2, belt-and-braces 23-DEC-2003 MGD v1.6.1, minor conditional mods to support IA64 08-MAR-2003 MGD v1.6.0, add '100 REASON text' for username/password failure 28-FEB-2001 MGD v1.5.0, example of /NO401 functionality 01-FEB-2001 MGD v1.4.0, add VMS-USER and demonstration capability 18-JAN-2001 MGD v1.3.0, an agent can now '100 REMOTE-USER username' 06-DEC-2000 MGD v1.2.0, an agent can now '100 SET-COOKIE rfc2109-cookie' 28-OCT-2000 MGD v1.1.0, use CGILIB object module 06-SEP-1999 MGD v1.0.0, initial development */ /*****************************************************************************/ #define SOFTWAREVN "2.0.0" #define SOFTWARENM "AUTHAGENT_EXAMPLE" #ifdef __ALPHA # define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN #endif #ifdef __ia64 # define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN #endif #ifdef __x86_64 # define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN #endif /* standard C header files */ #include #include #include #include #include #include /* VMS related header files */ #include #include #include /* application header files */ #include #define VMSok(x) ((x) & STS$M_SUCCESS) #define VMSnok(x) !(((x) & STS$M_SUCCESS)) #define boolean int #define true 1 #define false 0 /******************/ /* global storage */ /******************/ char Utility [] = "AUTHAGENT_EXAMPLE"; int CgiPlusUsageCount; boolean Debug, DebugWatch; char SoftwareId [64]; /*****************************************************************************/ /* */ main () { int status; char *cptr; /*********/ /* begin */ /*********/ sprintf (SoftwareId, "%s (%s)", SOFTWAREID, CgiLibEnvironmentVersion()); Debug = (getenv ("AUTHAGENT_EXAMPLE$DBUG") != NULL); DebugWatch = (getenv ("AUTHAGENT_EXAMPLE$WATCH") != NULL); CgiLibEnvironmentSetDebug (Debug); CgiLibEnvironmentInit (0, NULL, false); /* MUST only be executed in a CGIplus environment! */ if (!CgiLibEnvironmentIsCgiPlus ()) { CgiLibResponseHeader (502, "text/plain"); fputs ("CGIplus!\n", stdout); exit (SS$_NORMAL); } for (;;) { /* block waiting for the next request */ CgiLibVar (""); CgiPlusUsageCount++; if (cptr = CgiLibVarNull("REQUEST_METHOD")) if (!*cptr) { /* proctored into existance */ CgiLibResponseHeader (204, "application/proctor"); CgiLibCgiPlusEOF (); continue; } CgiLibCgiPlusCallout ("!AGENT-BEGIN: %s (%s) usage:%d", SoftwareId, CgiLibEnvironmentVersion(), CgiPlusUsageCount); ProcessRequest (); CgiLibCgiPlusCallout ("!AGENT-END:"); CgiLibCgiPlusEOF (); } } /*****************************************************************************/ /* Main authentication request processing function. */ ProcessRequest () { int status; char *AllowedClientPtr, *AuthAgentPtr, *PasswordPtr, *RemoteAddrPtr, *UserNamePtr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "ProcessRequest()\n"); /* provide the server attention "escape" sequence record */ if (!Debug) CgiLibCgiPlusESC (); /* ensure this is being invoked by the server */ if ((AuthAgentPtr = CgiLibVarNull ("WWW_AUTH_AGENT")) == NULL) exit (SS$_ABORT); /* belt and braces */ fprintf (stdout, "100 AUTHAGENT-CALLOUT\n"); fflush (stdout); if (DebugWatch) { /* just a comment that can be WATCHed */ fprintf (stdout, "000 [%d] %s\n", __LINE__, SoftwareId); fflush (stdout); } if (strsame (AuthAgentPtr, "REALM", -1)) { /* "standard" use of agent to authenticate user */ UserNamePtr = CgiLibVar ("WWW_REMOTE_USER"); PasswordPtr = CgiLibVar ("WWW_AUTH_PASSWORD"); AuthenticateUser (UserNamePtr, PasswordPtr); } else if (strsame (AuthAgentPtr, "GROUP", -1)) { /* "standard" use of agent to authorize group membership */ UserNamePtr = CgiLibVar ("WWW_REMOTE_USER"); MemberOfGroup (UserNamePtr); } else if (strsame (AuthAgentPtr, "/NO401", 6)) { /* non-user/pass authorization (example based on client IP address) */ AllowedClientPtr = getenv ("AUTHAGENT_EXAMPLE_NO401"); RemoteAddrPtr = CgiLibVar ("WWW_REMOTE_ADDR"); if (AllowedClientPtr == NULL || !strsame (AllowedClientPtr, RemoteAddrPtr, -1)) { /* logical name not defined or not the same as client IP address */ fprintf (stdout, "403 Not %s\n", AllowedClientPtr == NULL ? "defined!" : AllowedClientPtr); fflush (stdout); } else { /* logical name is the same as client IP address */ fprintf (stdout, "200 READ+WRITE\n"); fflush (stdout); } } else { /* HTTPD$AUTH "param=" used to pass authentication source file name */ UserNamePtr = CgiLibVar ("WWW_REMOTE_USER"); PasswordPtr = CgiLibVar ("WWW_AUTH_PASSWORD"); AuthenticateFromList (AuthAgentPtr, UserNamePtr, PasswordPtr); } /* provide the "escape" end-of-text sequence record */ if (!Debug) CgiLibCgiPlusEOT (); } /*****************************************************************************/ /* Example username/password verification function. */ AuthenticateUser ( char *UserNamePtr, char *PasswordPtr ) { static struct { char *UserName; char *Password; char *Details; boolean ReturnCookie; boolean VmsUser; } UserNamePasswordPairs [] = { { "username", "password", "(just to show which is which)", false, false }, { "mark", "daniel", "Mark Daniel, +61 8 82596031", false, false }, { "daniel", "daniel", "Mark Daniel (VMS-USER)", false, true }, { "no-such-user", "no-such-user", "fails (VMS-USER)", false, true }, { "moe", "howard", "Moses Horwitz", true, false }, { "larry", "fine", "Larry Fine", true, false }, { "curly", "howard", "Jakob Horwitz", true, false }, { NULL, NULL } }; int idx; char *cptr; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "AuthenticateUser() |%s|%s|\n", UserNamePtr, PasswordPtr); if (DebugWatch) { /* just a comment that can be WATCHed */ fprintf (stdout, "000 [%d] authenticating \"%s\"\n", __LINE__, UserNamePtr); fflush (stdout); } cptr = NULL; for (idx = 0; UserNamePasswordPairs[idx].UserName != NULL; idx++) { if (!strsame (UserNamePtr, UserNamePasswordPairs[idx].UserName, -1)) continue; cptr = "Password validation failure"; if (!strsame (PasswordPtr, UserNamePasswordPairs[idx].Password, -1)) continue; cptr = NULL; /* both the string and the status code are valid here */ fprintf (stdout, "200 READ+WRITE\n"); fflush (stdout); /* supply an informational - the user's details */ fprintf (stdout, "100 USER %s\n", UserNamePasswordPairs[idx].Details); fflush (stdout); if (UserNamePasswordPairs[idx].VmsUser) { /* force this to be a VMS user (just for demonstration purposes) */ fprintf (stdout, "100 VMS-USER %s\n", UserNamePasswordPairs[idx].UserName); fflush (stdout); } if (UserNamePasswordPairs[idx].ReturnCookie) { /* supply a cookie (just for demonstration purposes) */ fprintf (stdout, "100 SET-COOKIE AUTHAGENT_EXAMPLE=\"%s\"\n", SoftwareId); fflush (stdout); } return; } if (!cptr) cptr = "Username unknown"; /* doesn't matter what the string is, only the status code is checked */ fprintf (stdout, "100 REASON %s\n", cptr); fflush (stdout); fprintf (stdout, "401 NOT authenticated\n"); fflush (stdout); } /*****************************************************************************/ /* Example group membership function. */ MemberOfGroup (char *UserNamePtr) { static char *ThreeStooges [] = { "moe", "larry", "curly", NULL }; int idx; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "MemberOfGroup() |%s|\n", UserNamePtr); if (DebugWatch) { /* just a comment that can be WATCHed */ fprintf (stdout, "000 [%d] checking group membership of \"%s\"\n", __LINE__, UserNamePtr); fflush (stdout); } for (idx = 0; ThreeStooges[idx] != NULL; idx++) { if (!strsame (UserNamePtr, ThreeStooges[idx], -1)) continue; /* doesn't matter what the string is, only the status code is checked */ fprintf (stdout, "200 YES\n"); fflush (stdout); return; } /* doesn't matter what the string is, only the status code is checked */ fprintf (stdout, "403 NO\n"); fflush (stdout); } /*****************************************************************************/ /* Example authentication from list kept in external file function. 'ListFileNamePtr' ("WWW_AUTH_AGENT", from "param=") contains the file name. This simple example just looks for the user name and compares any plain-text password it finds equated to it (e.g. "mark=daniel"). Any text that follows the white-space delimited password is used as the user detail (.e.g "mark=daniel Mark Daniel, Mark.Daniel@dsto.defence.gov.au"). */ AuthenticateFromList ( char *ListFileNamePtr, char *UserNamePtr, char *PasswordPtr ) { register char *sptr, *cptr; boolean EndOfFile; int idx; char Line [256]; FILE *ListFile; /*********/ /* begin */ /*********/ if (Debug) fprintf (stdout, "AuthenticateFromList() |%s|%s|%s|\n", ListFileNamePtr, UserNamePtr, PasswordPtr); if (DebugWatch) { /* just a comment that can be WATCHed */ fprintf (stdout, "000 [%d] authenticating \"%s\" from \"%s\"\n", __LINE__, UserNamePtr, ListFileNamePtr); fflush (stdout); } if (!ListFileNamePtr[0]) { /* error string will be reported by the server */ fprintf (stdout, "500 Database not specified.\n"); return; } if ((ListFile = fopen (ListFileNamePtr, "r", "shr=get")) == NULL) { /* error string will be reported by the server */ fprintf (stdout, "500 Database open error %%X%08.08X\n", vaxc$errno); return; } while (EndOfFile = (fgets (Line, sizeof(Line), ListFile) != NULL)) { if (Debug) fprintf (stdout, "|%s|\n", Line); /* skip leading white-space */ for (cptr = Line; *cptr && isspace(*cptr); cptr++); /* ignore comment lines */ if (*cptr == '#' || *cptr == '!') continue; /* case sensitive comparison of user names */ sptr = UserNamePtr; while (*cptr && *cptr != '=' && !isspace(*cptr) && *sptr) { if (*cptr != *sptr) break; cptr++; sptr++; } if (Debug) fprintf (stdout, "|%s|%s|\n", cptr, sptr); /* if not matched, continue */ if (!*cptr || *cptr != '=' || *sptr) continue; /* case sensitive comparison of passwords */ cptr++; sptr = PasswordPtr; while (*cptr && !isspace(*cptr) && *sptr) { if (*cptr != *sptr) break; cptr++; sptr++; } if (Debug) fprintf (stdout, "|%s|%s|\n", cptr, sptr); /* password verification succeeded! */ if ((!*cptr || isspace(*cptr)) && !*sptr) { /* both the string and the status code are valid here */ fprintf (stdout, "200 READ+WRITE\n"); fflush (stdout); /* use anything that follows as the user detail */ while (*cptr && isspace(*cptr)) cptr++; if (*cptr) { /* supply an informational - the user's details */ for (sptr = cptr; *sptr && *sptr != '\n'; sptr++); *sptr = '\0'; fprintf (stdout, "100 USER %s\n", cptr); fflush (stdout); } break; } /* doesn't matter what the string is, only the status code is checked */ fprintf (stdout, "401 \"%s\" NOT authenticated\n", UserNamePtr); fflush (stdout); break; } EndOfFile = !EndOfFile; if (EndOfFile) { /* no such user name found */ /* doesn't matter what the string is, only the status code is checked */ fprintf (stdout, "401 \"%s\" NOT found\n", UserNamePtr); fflush (stdout); } fclose (ListFile); } /****************************************************************************/ /* Does a case-insensitive, character-by-character string compare and returns true if two strings are the same, or false if not. If a maximum number of characters are specified only those will be compared, if the entire strings should be compared then specify the number of characters as 0. */ boolean strsame ( char *sptr1, char *sptr2, int count ) { while (*sptr1 && *sptr2) { if (toupper (*sptr1++) != toupper (*sptr2++)) return (false); if (count) if (!--count) return (true); } if (*sptr1 || *sptr2) return (false); else return (true); } /*****************************************************************************/