Commit dc129fba authored by ale's avatar ale

Add vendor dependencies

parent 24de79bb
Copyright (c) 2017 Autistici/Inventati <info@autistici.org>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
A/I Auth Server
===============
An authentication server with an LDAP backend and some advanced
features:
* OTP support
* application-specific passwords
* rate limiting and brute force protection
* user partitioning across multiple backends
The authentication server can be queried using an HTTP API. The
software includes a PAM module (pam_authclient) that can be used
to integrate system services.
# Configuration
The authentication server data model is based on the concept of a
*user account*. The server knows how to retrieve user accounts stored
in LDAP, but it has to be told the specific details of how to find
them and how to map the information there to what it needs.
The server's behavior can be configured with a Python file defining
some module-level variables:
* `LDAP_URI` is the LDAP connection string.
* `LDAP_BIND_DN` and `LDAP_BIND_PW` specify the bind credentials: they
should allow the server to read authentication-related attributes on
the user objects, and to search the user subtree.
* `LDAP_SERVICE_MAP` is a `{service: query}` dictionary that defines
how to query the LDAP database for user accounts, for each service.
* `LDAP_SCHEMA` is a dictionary that describes how to retrieve
authentication information from the LDAP object attributes.
## Query definition
LDAP queries are meant to return a single user account object from the
database using a *search* operation. They are represented as
dictionaries, with the following fields:
* `dn`: specifies the full DN of the object
* `base`: specifies a base DN for the search
* `filter`: specifies a filter to apply to the search
The `dn` and `base` attributes are mutually exclusive. In fact,
queries can be of two types:
* If you can know the specific DN of a user account object given its
username, specify the `dn` field. This will run a SCOPE_BASE query
for that specific DN. In this case, `filter` is optional.
* Specify `base` and `filter` together to identify a single object.
This will result in a SCOPE_SUBTREE search starting at `base`. In
this case the `filter` attribute is required.
On every incoming request, the query fields are subject to string
substitution, using the standard Python syntax of the `%` operator,
with the following client-provided variables available:
* `user`: username
* `service`: authentication service
* `shard`: user shard
So, for example, a mail service could use the following query:
{
'base': 'ou=Accounts,dc=example,dc=com',
'filter': 'mail=%(user)s'
}
or alternatively, if the database structure is simple enough:
{
'dn': 'mail=%(user)s,ou=Accounts,dc=example,dc=com'
}
## Schema definition
In order to retrieve authentication information from the LDAP object,
the authentication server needs to know which attributes to use. To do
so, we use a so-called *schema definition* (a map of symbolic names to
LDAP attributes). The following attribute names are defined:
* `password`: attribute containing the encrypted password. Usually
(and by default) this is `userPassword`, a somewhat standard LDAP
attribute. Since this attribute is often also used for
authentication of the LDAP protocol itself, an eventual `{crypt}`
prefix is ignored. Passwords should be encrypted with the system
`crypt` method.
* `otp_secret`: this attribute should contain the hex-encoded TOTP
secret.
* `app_specific_password`: attribute (possibly defined more than once)
containing an encoded app-specific password.
* `shard`: if set, LDAP attribute containing a shard ID for user
partitioning.
The LDAP backend module makes some assumptions on the structure of the
user database: the most important is that app-specific passwords are
attributes of the user object. App-specific passwords should be
encoded as colon-separated strings:
service:encrypted_password:comment
Again, the password should be encrypted with the system `crypt`
method. The comment is a free-form string set by the user to tell the
various credentials apart.
## OTP implementation
The authentication server uses a very simple implementation of
time-based OTP (TOTP), supporting a single secret per user and without
any fancy features such as emergency tokens etc. The reason for this
is that TOTP authentication requires just plain read-only access to
the user database, while counter-based authentication with proper
token revocation is a read-write, locked operation which is more
difficult to perform on a LDAP backend.
# Usage
## Client authentication
Requests to the authentication server can themselves be authenticated,
and it may be a good idea to do so in a production environment.
Authserv supports TLS-based authentication using a private CA: in
order to perform authentication queries, a client must present an X509
certificate signed by a specific Certification Authority.
This feature is enabled by passing the `--ca`, `--ssl-cert` and
`--ssl-key` options to the server.
The PAM client has options to specify a client certificate.
## HTTP API
The authentication server accepts HTTP POST requests at the
`/api/1/auth` URL. Requests should contain the following parameters
(with a Content-Type of `application/x-www-form-urlencoded`):
* `service`: the service to authenticate for
* `username`: username
* `password`: password
* `otp_token`: a 6-digit TOTP token (optional)
* `source_ip`: the remote address of the client connection (optional)
* `shard`: server shard (optional)
The response is a simple string, one of:
* `OK`: the authentication request was successful
* `ERR_AUTHENTICATION_FAILURE`: the authentication request failed
* `ERR_OTP_REQUIRED`: an OTP token is required in order to
authenticate, but it was not provided in the request.
If the client application receives an `ERR_OTP_REQUIRED` result, it
should prompt the user for an OTP token and retry the authentication
request (including the same username and password) with the
`otp_token` field set. The PAM module, for example, can ask the user
for the OTP token interactively, while web applications could display
an interstitial form.
## PAM
A PAM module, `pam_authclient`, is provided to integrate system
services with the authentication server. It provides only the
*account* (a no-op) and *auth* operations.
Example usage:
auth required pam_authclient.so auth_server=127.0.0.1:1616
The module knows about the following options:
* `auth_server=`*HOST:PORT* specifies the authentication server to talk
to. It's possible to specify more than one server, separated by
commas, in which case they will be tried in sequence in case of
failure.
* `ssl_crt=`*FILE* load the X509 client certificate from FILE (in PEM format).
* `ssl_key=`*FILE* load the X509 certificate key from FILE.
* `ca=`*FILE* should point at the X509 CA certificate, used to verify
the authenticity of the server connection: the server certificate
must be signed by this CA.
* `shard=`*ID* sets the *shard* parameter, which may be used by the
authentication server to limit authentication to a subset of users.
The `service` parameter sent to the auth server will be the PAM service,
it is not possible to override this in the PAM configuration.
## NGINX mail_http_auth support
The authentication server can optionally offer an HTTP API compatible
with NGINX's `mail_http_auth` module. This is particularly useful in
combination with user partitioning, as the server can use the `shard`
attribute of the user to direct NGINX to the right backend.
The module understands the following configuration variables:
* `NGINX_AUTH_SERVICE` (default: `mail`) is the service that will be
used for authentication by this module.
* `NGINX_AUTH_PORT_MAP` is a dictionary that maps protocol names to
port numbers for the backend connection. The default is
`{"pop3": 110, "imap": 143}`.
package client
import (
"bytes"
"context"
"io"
"net/textproto"
"git.autistici.org/id/auth"
)
var DefaultSocketPath = "/run/auth/socket"
type Client interface {
Authenticate(context.Context, *auth.Request) (*auth.Response, error)
}
type socketClient struct {
socketPath string
codec auth.Codec
}
func New(socketPath string) Client {
return &socketClient{
socketPath: socketPath,
codec: auth.DefaultCodec,
}
}
func (c *socketClient) Authenticate(ctx context.Context, req *auth.Request) (*auth.Response, error) {
conn, err := textproto.Dial("unix", c.socketPath)
if err != nil {
return nil, err
}
defer conn.Close()
var resp auth.Response
done := make(chan error)
go func() {
defer close(done)
// Write the command to a buffer.
var buf bytes.Buffer
io.WriteString(&buf, "auth ")
buf.Write(c.codec.Encode(req))
if err := conn.PrintfLine(buf.String()); err != nil {
done <- err
return
}
// Read the response.
line, err := conn.ReadLineBytes()
if err != nil {
done <- err
return
}
if err := c.codec.Decode(line, &resp); err != nil {
done <- err
return
}
done <- nil
}()
// Wait for the call to terminate, or the context to time out,
// whichever happens first.
select {
case err := <-done:
return &resp, err
case <-ctx.Done():
return nil, ctx.Err()
}
}
package auth
import (
"bytes"
"encoding/base64"
"fmt"
"io"
)
// Codec serializes simple key/value maps on the wire. Requirements
// for a codec: it must transfer maps, it must be trivial to implement
// in both C, Go and Python. It must not have messy external
// dependencies if possible.
type Codec interface {
Encode(interface{}) []byte
Decode([]byte, interface{}) error
}
type mapEncodable interface {
EncodeToMap(map[string]string, string)
}
type mapDecodable interface {
DecodeFromMap(map[string]string, string)
}
type kvCodec struct{}
// DefaultCodec is the codec used by the on-wire protocol.
var DefaultCodec = &kvCodec{}
//var charsToEscape = "\" \r\n"
func shouldEscapeString(s string) bool {
for _, r := range s {
if r < 32 || r > 127 || r == '"' || r == ' ' || r == '\r' || r == '\n' {
return true
}
}
return false
}
func (c kvCodec) encodeMap(obj map[string]string) []byte {
var buf bytes.Buffer
first := true
for key, value := range obj {
if first {
first = false
} else {
io.WriteString(&buf, " ")
}
fmt.Fprintf(&buf, "%s=", key)
if shouldEscapeString(value) {
w := base64.NewEncoder(base64.RawURLEncoding, &buf)
io.WriteString(w, value)
w.Close()
} else {
fmt.Fprintf(&buf, "\"%s\"", value)
}
}
return buf.Bytes()
}
func (c kvCodec) Encode(obj interface{}) []byte {
var m map[string]string
switch t := obj.(type) {
case map[string]string:
m = t
case mapEncodable:
m = make(map[string]string)
t.EncodeToMap(m, "")
default:
// TODO: Error
}
return c.encodeMap(m)
}
type inputScanner struct {
b []byte
pos int
}
func (i *inputScanner) next() (byte, bool) {
if i.pos >= len(i.b) {
return 0, true
}
value := i.b[i.pos]
i.pos++
return value, false
}
func (i *inputScanner) ungetc() {
if i.pos > 0 {
i.pos--
}
}
func (i *inputScanner) skipWhitespace() {
for {
c, eof := i.next()
if eof {
return
}
if c != ' ' {
break
}
}
i.ungetc()
}
func (i *inputScanner) parseUntil(sep byte) ([]byte, error) {
pos := i.pos
idx := bytes.IndexByte(i.b[pos:], sep)
if idx == -1 {
return nil, io.EOF
}
value := i.b[i.pos : i.pos+idx]
i.pos += idx + 1
return value, nil
}
func (i *inputScanner) parseUntilOrEOF(sep byte) []byte {
pos := i.pos
idx := bytes.IndexByte(i.b[pos:], sep)
if idx == -1 {
i.pos = len(i.b)
} else {
i.pos = pos + idx
}
return i.b[pos:i.pos]
}
func (i *inputScanner) parseKey() (string, error) {
i.skipWhitespace()
key, err := i.parseUntil('=')
if err != nil {
return "", err
}
return string(key), nil
}
func (i *inputScanner) parseQuotedString() (string, error) {
s, err := i.parseUntil('"')
if err != nil {
return "", err
}
return string(s), nil
}
func (i *inputScanner) parseBase64String() (string, error) {
data := i.parseUntilOrEOF(' ')
out := make([]byte, base64.RawURLEncoding.DecodedLen(len(data)))
_, err := base64.RawURLEncoding.Decode(out, data)
if err != nil {
return "", err
}
return string(out), nil
}
func (i *inputScanner) parseValue() (string, error) {
c, eof := i.next()
if eof {
return "", io.EOF
}
if c == '"' {
return i.parseQuotedString()
}
i.ungetc()
return i.parseBase64String()
}
func (i *inputScanner) parseAssignment() (string, string, error) {
key, err := i.parseKey()
if err == io.EOF {
return "", "", nil
} else if err != nil {
return "", "", err
}
value, err := i.parseValue()
if err != nil {
return "", "", err
}
return key, value, nil
}