Created
September 8, 2011 02:19
-
-
Save leafstorm/1202447 to your computer and use it in GitHub Desktop.
UnWRAP, a WRAP authentication relay for NCSU. Actual UnWRAP script, Python library, and example Flask app with template. Released under the MIT license.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>UnWRAP Test</title> | |
<style type="text/css"> | |
p.flash { | |
font-style: italic; | |
text-align: center; | |
} | |
p.message { | |
font-size: xx-large; | |
font-weight: bold; | |
text-align: center; | |
} | |
p.command { | |
font-size: x-large; | |
font-style: italic; | |
text-align: center; | |
} | |
</style> | |
</head> | |
<body> | |
<h1>UnWRAP Test</h1> | |
{% for message in get_flashed_messages() %} | |
<p class="flash">{{ message }}</p> | |
{% endfor %} | |
<p> | |
This tests using the UnWRAP service to log in to non-ncsu.edu | |
sites using NCSU Unity credentials. | |
</p> | |
<p> | |
<strong>This does not steal your password or other sensitive | |
information.</strong> | |
The UnWRAP service does not have access to your Unity password or | |
any other sensitive personal information such as your campus ID | |
number, and it only | |
passes your Unity ID along to registered client Web sites. In | |
addition, UnWRAP uses cryptographic hash algorithms to prevent | |
data from being tampered with in transit. | |
</p> | |
<p> | |
<strong>Warning: IPv6 will probably break things.</strong> The | |
UnWRAP protocol requires matching addresses on the client server | |
and the UnWRAP server, and the UnWRAP server | |
(people.engr.ncsu.edu) doesn't speak IPv6. If you don't know what | |
IPv6 is, you probably don't need to worry about it. | |
</p> | |
<p> | |
If it works, you will see your Unity ID appear below after logging | |
in. If you encounter any problems, or have questions about how | |
the UnWRAP service is implemented — or you want to use it | |
in your application — please contact Matthew Frazier at | |
<em>mlfrazie at ncsu dot edu</em>. | |
</p> | |
{% if username %} | |
<p class="message">You are logged in as {{ username }}!</p> | |
<p class="command"><a href="{{ url_for('logout') }}">[Log out]</a></p> | |
{% else %} | |
<p class="message">You are not logged in...</p> | |
<p class="command"><a href="{{ url_for('login') }}">[Log in with UnWRAP]</a></p> | |
{% endif %} | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html> | |
<head> | |
<title>UnWRAP: LDAP/Privacy Info</title> | |
</head> | |
<body> | |
<h1>LDAP/Privacy Info</h1> | |
<p> | |
The purpose of the UnWRAP service is to pass your Unity ID | |
(username) on to an external Web site so that you can access | |
their services using your NCSU identity. UnWRAP was carefully | |
designed to avoid granting any information about you besides your | |
Unity ID to the external service; however, under the Family | |
Educational Rights and Privacy Act, your Unity ID counts as | |
sensitive information, and if you have a FERPA privacy block on | |
your account, transmitting it to an external service is a | |
violation of federal law. | |
</p> | |
<p> | |
Unfortunately, WRAP does not provide any indication of whether a | |
person's account has a privacy block on it. (Ironically, this | |
means that WRAP itself has the potential to violate FERPA.) So, | |
in order to verify a person's privacy status, after authenticating | |
them through WRAP, UnWRAP looks up that person's account record | |
on the <a href="http://www.ldap.ncsu.edu/">NCSU LDAP server</a> | |
at ldap.ncsu.edu. If a privacy block is set, then their account | |
record will not appear, and so UnWRAP will refuse access. | |
</p> | |
<p> | |
So if you receive a message stating, | |
"Could not find a record for your Unity ID | |
on the NCSU LDAP server," then that means that either (a) you | |
have a FERPA privacy block on your records, in which case UnWRAP | |
is not allowed to authorize you to the external service, or (b) | |
something is wrong with your account information, in which case | |
you will need to contact the <a href="http://oit.ncsu.edu/">Office | |
of Information Technology</a>. | |
</p> | |
<p> | |
If an error message is displayed saying that UnWRAP could not | |
connect to, bind to, or search for your account record on the | |
NCSU LDAP server, then I apologize, but when federal privacy law | |
is involved, one is "better safe than sorry." | |
</p> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// UnWRAP version 1.0 | |
// (C) 2011 Matthew Frazier | |
// Released under the MIT/X11 license. | |
//echo("It loaded!<br>"); | |
// Secret Keys | |
// Generate by running: | |
// python -c 'import os, base64; print base64.urlsafe_b64encode(os.urandom(32))' | |
$secretKeys = array( | |
'localhost' => 'development secret key - not for production use', | |
'unwraptest.ep.io' => '[REDACTED]' | |
); | |
function dlog($mesg) { | |
//echo($mesg . "<br>"); | |
}; | |
function setStatus($code) { | |
header("HTTP/1.1 $code"); | |
}; | |
function messageHTML($category) { | |
echo("<!doctype html>\n"); | |
echo("<title>UnWRAP: $category</title>\n\n"); | |
echo("<h1>UnWRAP: $category</h1>\n\n"); | |
}; | |
function errorMessage($mesg, $code = "400 Bad Request") { | |
setStatus($code); | |
messageHTML("Error"); | |
echo("<p>$mesg</p>"); | |
exit(); | |
}; | |
function ldapError($mesg, $code = "500 Internal Server Error") { | |
errorMessage($mesg . " (See <a href='ldap.html'>here</a> for details.)", | |
$code); | |
}; | |
// Step 1: Load URL parameters. | |
function getDefined($name) { | |
return (isset($_GET[$name]) && !empty($_GET[$name])); | |
}; | |
if (!(getDefined("next") && getDefined("ip") && getDefined("time") && | |
getDefined("sig"))) { | |
errorMessage("Not all required parameters were provided."); | |
}; | |
$reqNext = $_GET["next"]; | |
$reqIP = $_GET["ip"]; | |
$reqTime = $_GET["time"]; | |
$reqSig = $_GET["sig"]; | |
dlog("Request parsing complete."); | |
dlog("next: $reqNext; ip: $reqIP; time: $reqTime; sig: $reqSig"); | |
// Step 2: Verify the timestamp in time. | |
$givenTime = intval($reqTime); | |
$realTime = time(); | |
$timeDiff = abs($realTime - $givenTime); | |
dlog("The time is now $realTime. The time difference is $timeDiff."); | |
if ($timeDiff > 90) { | |
errorMessage("Timestamp is out of date. Please try logging in again."); | |
}; | |
dlog("Timestamp valid."); | |
// Step 3: Verify the IP address. | |
$realIP = $_SERVER["REMOTE_ADDR"]; | |
dlog("The requestor's IP is $realIP."); | |
if ($reqIP !== $realIP) { | |
errorMessage("IP address does not match."); | |
} | |
dlog("IP address valid."); | |
// Step 4: Check the secret key. | |
$domain = parse_url($reqNext, PHP_URL_HOST); | |
if (substr($domain, 0, 4) === "www.") { | |
$domain = substr($domain, 4); | |
}; | |
dlog("The domain in question is $domain."); | |
if (!isset($secretKeys[$domain])) { | |
errorMessage("The returning site is not registered with UnWRAP."); | |
}; | |
$secretKey = $secretKeys[$domain]; | |
// Step 5: Verify the signature. | |
$payload = "UnWRAP1+|$reqNext|$reqIP|$reqTime"; | |
$signature = hash_hmac("sha1", $payload, $secretKey); | |
if ($signature !== $reqSig) { | |
errorMessage("The request signature is invalid."); | |
}; | |
dlog("Okay, the request has been validated!"); | |
// Step 6: Load the WRAP information. | |
$username = $_SERVER['WRAP_USERID']; | |
$wrapAddress = $_SERVER['WRAP_ADDRESS']; | |
$wrapOnProxy = $_SERVER['WRAP_ONPROXY']; | |
dlog("Username: $username; Address: $wrapAddress; On proxy: $wrapOnProxy"); | |
// Step 7: Check the user against the LDAP directory. | |
$ldap = ldap_connect("ldap.ncsu.edu"); | |
if (!$ldap) { | |
ldapError("Could not connect to the NCSU LDAP server."); | |
}; | |
$bound = ldap_bind($ldap); | |
if (!$bound) { | |
ldapError("Could not bind to the NCSU LDAP server."); | |
}; | |
dlog("Connected and bound to LDAP server."); | |
$resultHandle = ldap_search($ldap, "ou=accounts,dc=ncsu,dc=edu", | |
"uid=$username"); | |
if (!$resultHandle) { | |
ldapError("There was an error searching for your account record on the " . | |
"NCSU LDAP server."); | |
}; | |
$count = ldap_count_entries($ldap, $resultHandle); | |
if ($count == 0) { | |
ldapError("Could not find a record for your Unity ID on the NCSU LDAP " . | |
"server.", "403 Unauthorized"); | |
}; | |
// Step 8: Build the credentials to return. | |
$ourSigPayload = "UnWRAP1=|$username|$reqIP|$realTime"; | |
$ourSignature = hash_hmac("sha1", $ourSigPayload, $secretKey); | |
$returnData = array( | |
'name' => $username, | |
'ip' => $reqIP, | |
'time' => $realTime, | |
'sig' => $ourSignature | |
); | |
$returnURL = $reqNext . '?' . http_build_query($returnData); | |
$safeURL = htmlspecialchars($returnURL); | |
// Step 9: Execute the redirect. | |
setStatus("303 See Other"); | |
header("Location: $returnURL"); | |
messageHTML("Authorized"); | |
echo("<p>We have verified your credentials successfully. " . | |
"<a href='$safeURL'>Click here</a> if you are not forwarded " . | |
"automatically.</p>"); | |
?> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import hmac | |
import urllib | |
import time | |
from hashlib import sha1 as hashfn | |
OFFICIAL_UNWRAP = "https://people.engr.ncsu.edu/mlfrazie/unwrap/unwrap.php" | |
class UnWRAP(object): | |
""" | |
This creates and verifies requests and responses to UnWRAP. | |
:param secret_key: The secret key registered with your domain on the | |
UnWRAP instance. | |
:param unwrap_url: The base URL of the UnWRAP script. It defaults to | |
`OFFICIAL_UNWRAP`, which is on people.engr.ncsu.edu | |
and uses HTTPS. | |
""" | |
def __init__(self, secret_key, unwrap_url=OFFICIAL_UNWRAP): | |
self.unwrap_url = unwrap_url | |
self.secret_key = secret_key | |
def build_request(self, next_url, ip): | |
""" | |
Builds the variables that need to go into the query string of an | |
UnWRAP request. It returns them as a `dict`. | |
:param next_url: The URL to send the credentials back to. | |
:param ip: The IP address of the user you are verifying. (Note: IPv4 | |
mixed with IPv6 breaks things.) | |
""" | |
timestamp = str(int(time.time())) | |
sig = self.sign("UnWRAP1+", next_url, ip, timestamp) | |
return dict(next=next_url, ip=ip, time=timestamp, sig=sig) | |
def build_url(self, next_url, ip): | |
""" | |
Builds a complete URL suitable for injecting in the ``Location:`` | |
header of a 303 See Other response. | |
:param next_url: The URL to send the credentials back to. | |
:param ip: The IP address of the user you are verifying. (Note: IPv4 | |
mixed with IPv6 breaks things.) | |
""" | |
return self.unwrap_url + "?" + urllib.urlencode( | |
self.build_request(next_url, ip) | |
) | |
def verify(self, query, timelimit=10): | |
""" | |
Takes a `dict` (though anything with a ``get(key)`` method will work) | |
containing the query string parameters returned to your application | |
by UnWRAP. It verifies everything except the IP address and returns | |
a tuple of ``(username, ip)`` if it checks out and ``(None, None)`` | |
if not. | |
:param query: The `dict` of query parameters. | |
:param timelimit: The maximum difference to accept between the | |
timestamp generated by UnWRAP and the current time | |
according to this computer. It defaults to 10 | |
seconds. | |
""" | |
name = query.get("name") | |
ip = query.get("ip") | |
timestamp = query.get("time") | |
sig = query.get("sig") | |
if not (name and ip and timestamp and sig): | |
return (None, None) | |
timediff = abs(int(time.time()) - int(timestamp)) | |
if timediff > timelimit: | |
return (None, None) | |
verifysig = self.sign("UnWRAP1=", name, ip, timestamp) | |
if sig != verifysig: | |
return (None, None) | |
return name, ip | |
def sign(self, *args): | |
""" | |
Concatenates all of the arguments passed to it, separated by ``|`` | |
symbols, and takes the SHA-1-backed HMAC digest of them, using | |
this instance's secret key. | |
:param args: The stuff to hash. | |
""" | |
hash = hmac.new(self.secret_key, "|".join(args), hashfn) | |
return hash.hexdigest() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
from flask import (Flask, request, url_for, redirect, session, | |
render_template, abort, flash) | |
from unwrap import UnWRAP | |
SECRET_KEY = "[REDACTED]" | |
app = Flask(__name__) | |
app.debug = True | |
app.secret_key = SECRET_KEY # too lazy to make a separate key | |
unwrap = UnWRAP(SECRET_KEY) | |
def de_ipv6(address): | |
# ep.io's servers add a ::ffff: on front of the IPv4 address | |
# this gets rid of it | |
if address.startswith("::ffff:"): | |
return address[7:] | |
return address | |
@app.route('/') | |
def index(): | |
return render_template("index.html", username=session.get("username")) | |
@app.route('/login') | |
def login(): | |
url = unwrap.build_url(url_for("accept", _external=True), | |
de_ipv6(request.remote_addr)) | |
return redirect(url, 303) | |
@app.route('/accept') | |
def accept(): | |
username, ip = unwrap.verify(request.args) | |
if username is None or ip != de_ipv6(request.remote_addr): | |
flash(u"There was a problem receiving your credentials from UnWRAP. " | |
"Please try again.") | |
else: | |
session['username'] = username | |
flash(u"Welcome, %s! You have been logged in." % username) | |
print "%s just logged in." % username | |
return redirect(url_for('index')) | |
@app.route('/logout') | |
def logout(): | |
if 'username' in session: | |
del session['username'] | |
flash(u"You were logged out.") | |
return redirect(url_for('index')) | |
if __name__ == '__main__': | |
app.run(debug=True) |
Coming soon: LDAP validation of Unity IDs to mitigate FERPA compliance issues.
LDAP support was added in revision afca95. If the user is not registered with the LDAP server (i.e. they have a privacy block on their records), they will not be authorized to the external service. Details provided to the user are available in ldap.html.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WARNING: PENDING REVIEW BY OIT AND ITECS, DEPLOYING THIS CODE IS LIKELY TO RESULT IN PAIN FOR YOU IN THE FORM OF SECURITY AND FERPA COMPLIANCE ISSUES. DON'T USE IT UNTIL FURTHER NOTICE.