mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 08:15:18 +00:00
Add token based auth for profiles
This commit is contained in:
parent
9a793088c6
commit
0344141faf
@ -190,6 +190,7 @@ def certidude_app():
|
||||
from .tag import TagResource, TagDetailResource
|
||||
from .attrib import AttributeResource
|
||||
from .bootstrap import BootstrapResource
|
||||
from .token import TokenResource
|
||||
|
||||
app = falcon.API(middleware=NormalizeMiddleware())
|
||||
app.req_options.auto_parse_form_urlencoded = True
|
||||
@ -202,7 +203,9 @@ def certidude_app():
|
||||
app.add_route("/api/request/{cn}/", RequestDetailResource())
|
||||
app.add_route("/api/request/", RequestListResource())
|
||||
app.add_route("/api/", SessionResource())
|
||||
app.add_route("/api/bootstrap/", BootstrapResource())
|
||||
|
||||
if config.BUNDLE_FORMAT and config.USER_ENROLLMENT_ALLOWED:
|
||||
app.add_route("/api/token/", TokenResource())
|
||||
|
||||
# Extended attributes for scripting etc.
|
||||
app.add_route("/api/signed/{cn}/attr/", AttributeResource())
|
||||
@ -217,8 +220,7 @@ def certidude_app():
|
||||
# Gateways can submit leases via this API call
|
||||
app.add_route("/api/lease/", LeaseResource())
|
||||
|
||||
# Optional user enrollment API call
|
||||
if config.USER_ENROLLMENT_ALLOWED:
|
||||
app.add_route("/api/bundle/", BundleResource())
|
||||
# Bootstrap resource
|
||||
app.add_route("/api/bootstrap/", BootstrapResource())
|
||||
|
||||
return app
|
||||
|
@ -1,44 +0,0 @@
|
||||
import logging
|
||||
import hashlib
|
||||
from certidude import config, authority
|
||||
from certidude.auth import login_required
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
KEYWORDS = (
|
||||
(u"Android", u"android"),
|
||||
(u"iPhone", u"iphone"),
|
||||
(u"iPad", u"ipad"),
|
||||
(u"Ubuntu", u"ubuntu"),
|
||||
(u"Fedora", u"fedora"),
|
||||
(u"Linux", u"linux"),
|
||||
(u"Macintosh", u"mac"),
|
||||
)
|
||||
|
||||
class BundleResource(object):
|
||||
@login_required
|
||||
def on_get(self, req, resp):
|
||||
common_name = req.context["user"].name
|
||||
if config.USER_MULTIPLE_CERTIFICATES:
|
||||
for key, value in KEYWORDS:
|
||||
if key in req.user_agent:
|
||||
device_identifier = value
|
||||
break
|
||||
else:
|
||||
device_identifier = u"unknown-device"
|
||||
common_name = u"%s@%s-%s" % (common_name, device_identifier, \
|
||||
hashlib.sha256(req.user_agent).hexdigest()[:8])
|
||||
|
||||
logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user"))
|
||||
if config.BUNDLE_FORMAT == "p12":
|
||||
resp.set_header("Content-Type", "application/x-pkcs12")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
|
||||
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
|
||||
owner=req.context.get("user"))
|
||||
elif config.BUNDLE_FORMAT == "ovpn":
|
||||
resp.set_header("Content-Type", "application/x-openvpn")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii"))
|
||||
resp.body, cert = authority.generate_ovpn_bundle(common_name,
|
||||
owner=req.context.get("user"))
|
||||
else:
|
||||
raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT)
|
94
certidude/api/token.py
Normal file
94
certidude/api/token.py
Normal file
@ -0,0 +1,94 @@
|
||||
import click
|
||||
import logging
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime
|
||||
from time import time
|
||||
from certidude import mailer
|
||||
from certidude.user import User
|
||||
from certidude import config, authority
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
chars = string.ascii_letters + string.digits + '!@#$%^&*()'
|
||||
SECRET = ''.join(random.choice(chars) for i in range(32))
|
||||
|
||||
click.echo("Token secret: %s" % SECRET)
|
||||
|
||||
|
||||
KEYWORDS = (
|
||||
(u"Android", u"android"),
|
||||
(u"iPhone", u"iphone"),
|
||||
(u"iPad", u"ipad"),
|
||||
(u"Ubuntu", u"ubuntu"),
|
||||
(u"Fedora", u"fedora"),
|
||||
(u"Linux", u"linux"),
|
||||
(u"Macintosh", u"mac"),
|
||||
)
|
||||
|
||||
class TokenResource(object):
|
||||
def on_get(self, req, resp):
|
||||
# Consume token
|
||||
now = time()
|
||||
timestamp = req.get_param_as_int("t", required=True)
|
||||
username = req.get_param("u", required=True)
|
||||
user = User.objects.get(username)
|
||||
csum = hashlib.sha256()
|
||||
csum.update(SECRET)
|
||||
csum.update(username)
|
||||
csum.update(str(timestamp))
|
||||
|
||||
if csum.hexdigest() != req.get_param("c", required=True):
|
||||
raise # TODO
|
||||
if now < timestamp:
|
||||
raise # Token not valid yet
|
||||
if now > timestamp + config.TOKEN_LIFETIME:
|
||||
raise # token expired
|
||||
# At this point consider token to be legitimate
|
||||
|
||||
common_name = username
|
||||
if config.USER_MULTIPLE_CERTIFICATES:
|
||||
for key, value in KEYWORDS:
|
||||
if key in req.user_agent:
|
||||
device_identifier = value
|
||||
break
|
||||
else:
|
||||
device_identifier = u"unknown-device"
|
||||
common_name = u"%s@%s-%s" % (common_name, device_identifier, \
|
||||
hashlib.sha256(req.user_agent).hexdigest()[:8])
|
||||
|
||||
logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user"))
|
||||
if config.BUNDLE_FORMAT == "p12":
|
||||
resp.set_header("Content-Type", "application/x-pkcs12")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
|
||||
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
|
||||
owner=req.context.get("user"))
|
||||
elif config.BUNDLE_FORMAT == "ovpn":
|
||||
resp.set_header("Content-Type", "application/x-openvpn")
|
||||
resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii"))
|
||||
resp.body, cert = authority.generate_ovpn_bundle(common_name,
|
||||
owner=req.context.get("user"))
|
||||
else:
|
||||
raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT)
|
||||
|
||||
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_post(self, req, resp):
|
||||
# Generate token
|
||||
issuer = req.context.get("user")
|
||||
username = req.get_param("user", required=True)
|
||||
user = User.objects.get(username)
|
||||
timestamp = int(time())
|
||||
csum = hashlib.sha256()
|
||||
csum.update(SECRET)
|
||||
csum.update(username)
|
||||
csum.update(str(timestamp))
|
||||
args = "u=%s&t=%d&c=%s" % (username, timestamp, csum.hexdigest())
|
||||
token_created = datetime.utcfromtimestamp(timestamp)
|
||||
token_expires = datetime.utcfromtimestamp(timestamp + config.TOKEN_LIFETIME)
|
||||
context = globals()
|
||||
context.update(locals())
|
||||
mailer.send("token.md", to=user, **context)
|
@ -44,8 +44,6 @@ EXPIRED_DIR = cp.get("authority", "expired dir")
|
||||
MAILER_NAME = cp.get("mailer", "name")
|
||||
MAILER_ADDRESS = cp.get("mailer", "address")
|
||||
|
||||
BUNDLE_FORMAT = cp.get("bundle", "format")
|
||||
OPENVPN_PROFILE_TEMPLATE = cp.get("bundle", "openvpn profile template")
|
||||
BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template")
|
||||
|
||||
MACHINE_ENROLLMENT_ALLOWED = {
|
||||
@ -96,4 +94,10 @@ else:
|
||||
|
||||
TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("tagging")]
|
||||
|
||||
# Tokens
|
||||
BUNDLE_FORMAT = cp.get("token", "format")
|
||||
OPENVPN_PROFILE_TEMPLATE = cp.get("token", "openvpn profile template")
|
||||
TOKEN_URL = cp.get("token", "url")
|
||||
TOKEN_LIFETIME = cp.getint("token", "lifetime")
|
||||
|
||||
# TODO: Check if we don't have base or servers
|
||||
|
@ -12,16 +12,17 @@ from urlparse import urlparse
|
||||
|
||||
env = Environment(loader=PackageLoader("certidude", "templates/mail"))
|
||||
|
||||
def send(template, to=None, attachments=(), **context):
|
||||
def send(template, to=None, include_admins=True, attachments=(), **context):
|
||||
from certidude import authority, config
|
||||
if not config.MAILER_ADDRESS:
|
||||
# Mailbox disabled, don't send e-mail
|
||||
return
|
||||
|
||||
recipients = u", ".join([unicode(j) for j in User.objects.filter_admins()])
|
||||
|
||||
recipients = ()
|
||||
if include_admins:
|
||||
recipients = tuple(User.objects.filter_admins())
|
||||
if to:
|
||||
recipients = to + u", " + recipients
|
||||
recipients = (to,) + recipients
|
||||
|
||||
click.echo("Sending e-mail %s to %s" % (template, recipients))
|
||||
|
||||
@ -29,12 +30,12 @@ def send(template, to=None, attachments=(), **context):
|
||||
html = markdown(text)
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["Subject"] = subject.encode("utf-8")
|
||||
msg["From"] = "%s <%s>" % (config.MAILER_NAME, config.MAILER_ADDRESS)
|
||||
msg["To"] = recipients
|
||||
msg["To"] = ", ".join([unicode(j) for j in recipients]).encode("utf-8")
|
||||
|
||||
part1 = MIMEText(text, "plain")
|
||||
part2 = MIMEText(html, "html")
|
||||
part1 = MIMEText(text.encode("utf-8"), "plain", "utf-8")
|
||||
part2 = MIMEText(html.encode("utf-8"), "html", "utf-8")
|
||||
|
||||
msg.attach(part1)
|
||||
msg.attach(part2)
|
||||
@ -44,6 +45,7 @@ def send(template, to=None, attachments=(), **context):
|
||||
part.add_header('Content-Disposition', 'attachment', filename=suggested_filename)
|
||||
part.set_payload(attachment)
|
||||
msg.attach(part)
|
||||
click.echo("Sending to: %s" % msg["to"])
|
||||
|
||||
conn = smtplib.SMTP("localhost")
|
||||
conn.sendmail(config.MAILER_ADDRESS, recipients, msg.as_string())
|
||||
conn.sendmail(config.MAILER_ADDRESS, [u.mail for u in recipients], msg.as_string())
|
||||
|
@ -1,5 +1,18 @@
|
||||
Stored request {{ common_name }}
|
||||
Token for setting up VPN
|
||||
|
||||
This is simply to notify that certificate signing request for {{ common_name }}
|
||||
was stored. You may log in with a certificate authority administration account to sign it.
|
||||
{{ issuer }} has provided {{ user }} a token for retrieving
|
||||
profile from the link below.
|
||||
|
||||
{% if config.BUNDLE_FORMAT == "ovpn" %}
|
||||
To set up OpenVPN for your device:
|
||||
|
||||
* for Android install [OpenVPN Connect](https://play.google.com/store/apps/details?id=de.blinkt.openvpn) app. After importing the OpenVPN profile in OpenVPN application and delete the downloaded .ovpn file.
|
||||
* for iOS device install [OpenVPN Connect](https://itunes.apple.com/us/app/openvpn-connect/id590379981) app. Tap on the token URL below, it should be automatically opened with OpenVPN Connect app. Tap connect to establish connection.
|
||||
* for Mac OS X download [Tunnelblick](https://tunnelblick.net/downloads.html)
|
||||
* for Ubuntu and Fedora install OpenVPN plugin for NetworkManager. Open network settings, add connection and select "Import from file ...". Supply the file retrieved via the token URL below.
|
||||
* for Windows you need to install OpenVPN community edition from [here](https://swupdate.openvpn.org/community/releases/openvpn-install-2.3.14-I601-x86_64.exe) and TAP driver from [here](https://swupdate.openvpn.org/community/releases/tap-windows-9.21.2.exe)
|
||||
{% endif %}
|
||||
|
||||
Click [here]({{ config.TOKEN_URL }}?{{ args }}) to claim the token.
|
||||
Token is usable until {{ token_expires }} (UTC).
|
||||
|
||||
|
@ -141,14 +141,6 @@ name = Certificate management
|
||||
address =
|
||||
;address = certificates@example.com
|
||||
|
||||
[bundle]
|
||||
format = p12
|
||||
;format = ovpn
|
||||
|
||||
# Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf
|
||||
# to /etc/certidude/ and make modifications as necessary
|
||||
openvpn profile template = {{ template_path }}/openvpn-client.conf
|
||||
|
||||
[tagging]
|
||||
owner/string = Owner
|
||||
location/string = Location
|
||||
@ -160,3 +152,18 @@ other/ = Other
|
||||
# Services template is rendered on certidude server with relevant variables and
|
||||
# placed to /etc/certidude/services.conf on the client
|
||||
services template = {{ template_path }}/bootstrap.conf
|
||||
|
||||
[token]
|
||||
# Token mechanism allows authority administrator to send invites for users.
|
||||
# Token URL could be for example exposed on the internet via proxypass.
|
||||
url = http://{{ common_name }}/api/token
|
||||
lifetime = 3600
|
||||
|
||||
# Profile format, uncomment specific one to enable token mechanism
|
||||
format =
|
||||
;format = p12
|
||||
;format = ovpn
|
||||
|
||||
# Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf
|
||||
# to /etc/certidude/ and make modifications as necessary
|
||||
openvpn profile template = {{ template_path }}/openvpn-client.conf
|
||||
|
Loading…
Reference in New Issue
Block a user