Monday, November 08, 2010 at 12:04 AM.
system.verbs.builtins.mainResponder.security.httpAuthentication
on httpAuthentication (realm, pta=nil, groupname="default", memberList=nil, securityLevel=0, fldebug=false, domain="/", noncesExpireAfterMinutes=15) {
<<Change Notes
<<Docs: http://docserver.userland.com/mainResponder/security/httpAuthentication
<<4/16/00; 1:21:20 AM by JES
<<localized all error strings
<<Tuesday, October 26, 1999 at 4:12:49 PM by AR
<<Basic Access Authentication as well as Digest Access Authentication are supported per RFC 2617
<<We assume that we are called from a mainResponder #security script. If we aren't, please pass us the address of the param table.
<<Sunday, November 07, 1999 at 9:26:41 PM by AR
<<username is now case-insensitive
<<Sunday, December 19, 1999 at 4:30:04 PM by AR
<<Closed a security hole in the Basic Authentication process.
<<05/01/00; 6:40:25 PM by JES
<<Changed getString calls to use a replacement table address instead of a lists
<<06/19/00; 9:55:54 AM by JES
<<localization bug fix: when outside wsf context, html.getPageTableAddress was failing in mainResponder.getString
<<Added support for Frontier language setting in user.prefs
<<4/16/00; 4:50:45 PM by JES: temporary page table for localization support
<<for localization, we have to have a page table with at least the language, so we make a temporary one
local (tempTable); new (tableType, @tempTable);
if pta != nil { // if in wsf context, use the language defined by the site, if any
if defined (pta^.language) {
tempTable.language = pta^.language}};
if not defined (tempTable.language) { // outside wsf context, or no language defined for site
if defined (config.mainResponder.globals.language) {
tempTable.language = config.mainResponder.globals.language}
else {
if defined (user.prefs.language) {
tempTable.language = user.prefs.language}
else {
tempTable.language = "English"}}};
on H (s) { //MD5 hash function
return (string.hashMD5 (s))};
on KD (secret, data) { //Section 3.2.1 of RFC 2617
return (H (secret + ":" + data))};
on A1 () { //Section 3.2.2.2 of RFC 2617
return (authTable.username + ":" + realm + ":" + adrMember^.password)};
on A2 () { //Section 3.2.2.3 of RFC 2617
if flAuthInt {
return (pta^.method + ":" + authTable.uri + ":" + H (pta^.requestBody))}
else {
return (pta^.method + ":" + authTable.uri)}};
on requestDigest () { //Section 3.2.2.1 of RFC 2617
if flAuth or flAuthInt {
return (KD (H (A1 ()), authTable.nonce + ":" + authTable.nc + ":" + authTable.cnonce + ":" + authTable.qop + ":" + H (A2 ())))}
else {
return (KD (H (A1 ()), authTable.nonce + ":" + H (A2 ())))}};
on initNonces () {
if not defined (adrNonces^) {
new (tableType, adrNonces)}};
on generateNonce () {
local (nonce, now = clock.now ());
nonce = H (now + ":" + temp.Frontier.startupTime + ":" + pta^.client + ":" + domain + ":" + memAvail () + ":" + string.padWithZeros (random (0, infinity), 8));
local (adrNonce = @adrNonces^.[nonce]);
new (tableType, adrNonce);
adrNonce^.nc = 1;
adrNonce^.expires = now + noncesExpireAfterMinutes * 60;
return (nonce)};
on deleteNonce () {
try {delete (@adrNonces^.[authTable.nonce])};
return (true)};
on httpUnauthorized (flStale=false) { //try again with proper credentials
on addToAuthHeader (s) {
local (adr = @pta^.responseHeaders.["WWW-Authenticate"]);
if defined (adr^) {
if typeOf (adr^) == listType {
adr^ = adr^ + {s}}
else {
adr^ = {adr^} + {s}}}
else {
adr^ = s};
return};
if flAcceptDigest {
initNonces ();
deleteNonce ();
local (s = "");
s = s + "Digest realm=\"" + realm + "\",";
s = s + " domain=\"" + domain + "\",";
s = s + " nonce=\"" + generateNonce () + "\",";
if flStale {
s = s + " stale=true,"};
s = s + " algorithm=MD5,";
s = s + " qop=\"auth,auth-int\"";
addToAuthHeader (s)};
if flAcceptBasic {
addToAuthHeader ("Basic realm=\"" + realm + "\"")};
pta^.responseBody = webserver.util.buildErrorPage ("401 UNAUTHORIZED", mainResponder.getString ("security.http401Error", pta: @tempTable)); // 4/16/00 JES: localized
pta^.code = 401;
if flDebug {
adrDebugTable^.["WWW-Authenticate"] = pta^.responseHeaders.["WWW-Authenticate"];
adrDebugTable^.code = pta^.code};
scriptError ("!return")}; //prevent mainResponder.respond from overwriting our response
on httpForbidden (s=nil) { //don't bother to try again, permanent failure
pta^.code = 403;
if s == nil {
s = mainResponder.getString ("security.http403Error", pta: @tempTable)}; // 4/16/00 JES: localized
pta^.responseBody = webserver.util.buildErrorPage ("403 FORBIDDEN", s);
if flDebug {
adrDebugTable^.code = pta^.code};
scriptError ("!return")}; //prevent mainResponder.respond from overwriting our response
on httpBadRequest (s=nil) { //don't bother to try again, malformed request
pta^.code = 400;
if s == nil {
s = mainResponder.getString ("security.http400Error", pta: @tempTable)}; // 4/16/00 JES: localized
pta^.responseBody = webserver.util.buildErrorPage ("400 BAD REQUEST", s);
if flDebug {
adrDebugTable^.code = pta^.code};
scriptError ("!return")}; //prevent mainResponder.respond from overwriting our response
on checkMember (username, password = nil) { //set adrMember or throw an error
adrMember = mainResponder.members.getMemberTable (groupname, username);
if not defined (adrMember^) {
httpUnauthorized ()};
if (password != nil) and (password != adrMember^.password) {
httpUnauthorized ()};
if memberList != nil { //make sure the client is in the specified list of members
local (lowerName = string.lower (username));
if typeOf (memberList) == listType {
local (memberName, flAuthorized = false);
for memberName in memberList {
if string.lower (memberName) == lowerName {
flAuthorized = true;
break}};
if not flAuthorized {
httpUnauthorized ()}}
else {
if string.lower (memberList) != lowerName {
httpUnauthorized ()}}}};
on fakeCookie (username, password) { //add a cookie to the request headers so mainResponder.members.checkMembership won't complain
if not defined (pta^.requestHeaders.cookies) {
new (tableType, @pta^.requestHeaders.cookies)};
local (adrMembers = mainResponder.members.getMembershipTable (groupname));
local (myCookieName = string.innerCaseName (adrMembers^.cookieName));
pta^.requestHeaders.cookies.[myCookieName] = string.urlEncode (username + "\t" + password)};
local (adrMember);
local (flAuth = false, flAuthInt = false); //our internal state, don't mess with these
local (adrNonces = @system.temp.httpDigestAuthentication);
local (authTable, adrDebugTable);
if pta == nil {
pta = parentOf (client)}; //assume we're called from a #security script
if flDebug {
adrDebugTable = log.addToGuestDatabase ("debugHttpAuthentication");
adrDebugTable^.securityLevel = securityLevel;
if defined (pta^.requestHeaders.Authorization) {
adrDebugTable^.Authorization = pta^.requestHeaders.Authorization};
if defined (pta^.requestHeaders.["User-Agent"]) {
adrDebugTable^.["User-Agent"] = pta^.requestHeaders.["User-Agent"]};
if defined (pta^.client) {
adrDebugTable^.client = pta^.client};
if defined (pta^.uri) {
adrDebugTable^.uri = pta^.uri}};
local (flAcceptDigest = false, flAcceptBasic = false, flOneTimeNonces = false);
if (securityLevel > 0) and (not defined (string.hashMD5)) { //for compatibility with 6.0
httpForbidden (mainResponder.getString ("security.httpUnsupportedSecurityLevel", pta: @tempTable))}; // 4/16/00 JES: localized
case securityLevel { //initialize the above based on specified security level
0 {
flAcceptBasic = true};
1 {
flAcceptBasic = true;
flAcceptDigest = true};
2 {
flAcceptDigest = true};
3 {
flAcceptDigest = true;
flOneTimeNonces = true}}
else {
httpForbidden (mainResponder.getString ("security.httpUnspecifiedSecurityLevel", pta: @tempTable))}; // 4/16/00 JES: localized
if not defined (pta^.requestHeaders.Authorization) {
httpUnauthorized ()};
local (s, authScheme);
s = string (pta^.requestHeaders.Authorization);
authScheme = string.nthField (string.lower (s), " ", 1);
s = string.trimWhiteSpace (string.delete (s, 1, string.length (authScheme)));
case authScheme {
"basic" {
if not flAcceptBasic {
httpUnauthorized ()};
local (credentials, value, username, password);
credentials = string.nthField (s, " ", 1);
credentials = string.popTrailing (s, ',');
value = string (base64.decode (credentials));
username = string.nthField (value, ":", 1);
password = string.delete (value, 1, string.length (username) + 1);
if flDebug {
adrDebugTable^.username = username;
adrDebugTable^.password = password};
if (string.length (username) == 0) or (string.length (password) == 0) { //AR 12/19/1999
httpUnauthorized ()};
checkMember (username, password);
fakeCookie (username, password)};
"digest" {
if not flAcceptDigest {
httpUnauthorized ()};
new (tableType, @authTable);
loop { //parse Authorization header
local (name, value);
bundle { //loop over white-space and delimiters
local (ix = 1);
while " \t," contains string.nthChar (s, ix) {
ix++};
s = string.mid (s, ix, string.length (s))};
bundle { //extract name
name = string.nthField (s, "=", 1);
if string.length (name) == 0 {
break};
s = string.delete (s, 1, string.length (name) + 1)};
bundle { //extract value
local (ix = 2);
if string.nthChar (s, 1) == "\"" {
while ix <= string.length (s) {
case s[ix] {
'"' {
value = string.mid (s, 2, ix-2);
value = string.replaceAll (value, "\\\"", "\""); //unquote
break};
'\\' {
ix++}};
ix++}}
else {
while ix <= string.length (s) {
case s[ix] {
' ';
'\t';
',' {
break}};
ix++};
value = string.mid (s, 1, ix-1)};
if sizeof (value) == 0 {
break};
s = string.delete (s, 1, ix)};
authTable.[name] = value};
if flDebug {
table.assign (@adrDebugTable^.authTable, authTable)};
local (replacementTable); new (tableType, @replacementTable); // 05/01/00 JES: use replacement table instead of a list
bundle { //check for bad requests
on checkRequiredField (field) {
if not defined (authTable.[field]) {
replacementTable.field = field;
httpBadRequest (mainResponder.getString ("security.httpMissingFieldError", @replacementTable, pta: @tempTable))}}; // 4/16/00 JES: localized
on checkForbiddenField (field) {
if defined (authTable.[field]) {
replacementTable.field = field;
httpBadRequest (mainResponder.getString ("security.httpIllegalFieldError", @replacementTable, pta: @tempTable))}}; // 4/16/00 JES: localized
checkRequiredField ("username");
checkRequiredField ("realm");
checkRequiredField ("nonce");
checkRequiredField ("uri");
bundle { //check URI
local (requestURI = string.nthField (pta^.firstLine, " ", 2));
if requestURI beginsWith "http://" { //it's a future-style full URL
local (pathParts = string.urlSplit (requestURI));
requestURI = "/" + pathParts[3]};
if requestURI != authTable.uri {
httpBadRequest (mainResponder.getString ("security.httpBadURIDirective", pta: @tempTable))}}; // 4/14/00 JES: localized
checkRequiredField ("response");
if defined (authTable.qop) {
local (qop = string.lower (authTable.qop));
flAuth = (qop == "auth");
flAuthInt = (qop == "auth-int");
if not (flAuth or flAuthInt) {
httpBadRequest (mainResponder.getString ("security.httpBadQopField", pta: @tempTable))}; // 4/14/00 JES: localized
checkRequiredField ("cnonce");
checkRequiredField ("nc")}
else {
checkForbiddenField ("cnonce");
checkForbiddenField ("nc")};
if defined (authTable.algorithm) and (authTable.algorithm != "MD5") {
httpBadRequest (mainResponder.getString ("security.httpUnsupportedDigestAlgorithm", pta: @tempTable))}}; // 4/14/00 JES: localized
if (string.length (authTable.username) == 0) { //AR 12/19/1999
httpUnauthorized ()};
checkMember (authTable.username);
initNonces ();
local (adrNonce = @adrNonces^.[authTable.nonce]);
if not defined (adrNonce^) {
httpUnauthorized ()};
if flAuth or flAuthInt { //check nonce-count
local (s = string.hex (adrNonce^.nc));
s = string.delete (s, 1, 2); //delete "0x"
s = string.filledString ("0", 8 - string.length (s)) + s;
s = string.lower (s);
if s != authTable.nc { //nonce count out of sequence
httpUnauthorized ()};
adrNonce^.nc++};
if authTable.response != requestDigest () {
httpUnauthorized ()};
if clock.now () > adrNonce^.expires { //is nonce stale?
httpUnauthorized (flStale:true)};
if flOneTimeNonces and not (flAuth or flAuthInt) { //send a new nonce, expire the current one
adrNonce^.expires = clock.now () - 1;
pta^.responseHeaders.["Authentication-Info"] = "nextnonce=\"" + generateNonce () + "\"";
if flDebug {
adrDebugTable^.["Authentication-Info"] = pta^.responseHeaders.["Authentication-Info"]}};
fakeCookie (authTable.username, adrMember^.password)}}
else {
httpUnauthorized ()};
return (true)}
This listing is for code that runs in the OPML Editor environment. I created these listings because I wanted the search engines to index it, so that when I want to look up something in my codebase I don't have to use the much slower search functionality in my object database. Dave Winer.