mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +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 .tag import TagResource, TagDetailResource
|
||||||
from .attrib import AttributeResource
|
from .attrib import AttributeResource
|
||||||
from .bootstrap import BootstrapResource
|
from .bootstrap import BootstrapResource
|
||||||
|
from .token import TokenResource
|
||||||
|
|
||||||
app = falcon.API(middleware=NormalizeMiddleware())
|
app = falcon.API(middleware=NormalizeMiddleware())
|
||||||
app.req_options.auto_parse_form_urlencoded = True
|
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/{cn}/", RequestDetailResource())
|
||||||
app.add_route("/api/request/", RequestListResource())
|
app.add_route("/api/request/", RequestListResource())
|
||||||
app.add_route("/api/", SessionResource())
|
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.
|
# Extended attributes for scripting etc.
|
||||||
app.add_route("/api/signed/{cn}/attr/", AttributeResource())
|
app.add_route("/api/signed/{cn}/attr/", AttributeResource())
|
||||||
@ -217,8 +220,7 @@ def certidude_app():
|
|||||||
# Gateways can submit leases via this API call
|
# Gateways can submit leases via this API call
|
||||||
app.add_route("/api/lease/", LeaseResource())
|
app.add_route("/api/lease/", LeaseResource())
|
||||||
|
|
||||||
# Optional user enrollment API call
|
# Bootstrap resource
|
||||||
if config.USER_ENROLLMENT_ALLOWED:
|
app.add_route("/api/bootstrap/", BootstrapResource())
|
||||||
app.add_route("/api/bundle/", BundleResource())
|
|
||||||
|
|
||||||
return app
|
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_NAME = cp.get("mailer", "name")
|
||||||
MAILER_ADDRESS = cp.get("mailer", "address")
|
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")
|
BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template")
|
||||||
|
|
||||||
MACHINE_ENROLLMENT_ALLOWED = {
|
MACHINE_ENROLLMENT_ALLOWED = {
|
||||||
@ -96,4 +94,10 @@ else:
|
|||||||
|
|
||||||
TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("tagging")]
|
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
|
# 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"))
|
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
|
from certidude import authority, config
|
||||||
if not config.MAILER_ADDRESS:
|
if not config.MAILER_ADDRESS:
|
||||||
# Mailbox disabled, don't send e-mail
|
# Mailbox disabled, don't send e-mail
|
||||||
return
|
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:
|
if to:
|
||||||
recipients = to + u", " + recipients
|
recipients = (to,) + recipients
|
||||||
|
|
||||||
click.echo("Sending e-mail %s to %s" % (template, 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)
|
html = markdown(text)
|
||||||
|
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject.encode("utf-8")
|
||||||
msg["From"] = "%s <%s>" % (config.MAILER_NAME, config.MAILER_ADDRESS)
|
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")
|
part1 = MIMEText(text.encode("utf-8"), "plain", "utf-8")
|
||||||
part2 = MIMEText(html, "html")
|
part2 = MIMEText(html.encode("utf-8"), "html", "utf-8")
|
||||||
|
|
||||||
msg.attach(part1)
|
msg.attach(part1)
|
||||||
msg.attach(part2)
|
msg.attach(part2)
|
||||||
@ -44,6 +45,7 @@ def send(template, to=None, attachments=(), **context):
|
|||||||
part.add_header('Content-Disposition', 'attachment', filename=suggested_filename)
|
part.add_header('Content-Disposition', 'attachment', filename=suggested_filename)
|
||||||
part.set_payload(attachment)
|
part.set_payload(attachment)
|
||||||
msg.attach(part)
|
msg.attach(part)
|
||||||
|
click.echo("Sending to: %s" % msg["to"])
|
||||||
|
|
||||||
conn = smtplib.SMTP("localhost")
|
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 }}
|
{{ issuer }} has provided {{ user }} a token for retrieving
|
||||||
was stored. You may log in with a certificate authority administration account to sign it.
|
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 =
|
||||||
;address = certificates@example.com
|
;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]
|
[tagging]
|
||||||
owner/string = Owner
|
owner/string = Owner
|
||||||
location/string = Location
|
location/string = Location
|
||||||
@ -160,3 +152,18 @@ other/ = Other
|
|||||||
# Services template is rendered on certidude server with relevant variables and
|
# Services template is rendered on certidude server with relevant variables and
|
||||||
# placed to /etc/certidude/services.conf on the client
|
# placed to /etc/certidude/services.conf on the client
|
||||||
services template = {{ template_path }}/bootstrap.conf
|
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