Skip to content

Instantly share code, notes, and snippets.

@leafstorm
Created September 8, 2011 02:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leafstorm/1202447 to your computer and use it in GitHub Desktop.
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.
<!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 &mdash; or you want to use it
in your application &mdash; 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>
<!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>
<?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>");
?>
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()
# -*- 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)
@leafstorm
Copy link
Author

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.

@leafstorm
Copy link
Author

Coming soon: LDAP validation of Unity IDs to mitigate FERPA compliance issues.

@leafstorm
Copy link
Author

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