Commit 252128ad authored by ale's avatar ale
Browse files

Split the login service from the SSO library

This removes a circular dependency between projects.
parent 62a3d97a
Pipeline #13270 passed with stage
in 24 seconds
include: "https://git.autistici.org/ai3/build-deb/raw/master/ci-nextstable.yml"
stages:
- test
run_tests:
stage: test
image: registry.git.autistici.org/ai3/docker/test/golang:master
script:
- run-go-test ./...
artifacts:
when: always
reports:
cobertura: cover.xml
junit: report.xml
sso
===
Login server (or *identity provider*, IDP) using the
[ai/sso](https://git.autistici.org/ai/sso) protocol (version 5) for
single-sign on and [auth-server](https://git.autistici.org/id/auth) to
authenticate users.
Native Golang implementation of the [ai/sso](https://git.autistici.org/ai/sso)
single sign-on mechanism.
This repository includes a few separate binaries:
This repository also used to include the login service, which has
now been split off into its own repository at [id/sso-server](https://git.autistici.org/id/sso-server).
* *sso-server* is the login server / IDP
* *saml-server* is a SSO-to-SAML bridge (for third-party software)
* *sso-proxy* is a reverse HTTP proxy that adds single-sign-on access
controls to backends
# Configuration
The *sso-server* daemon requires a YAML configuration file,
*/etc/sso/server.yml* by default. It understands the following
attributes:
* `secret_key_file`: path to the Ed25519 secret key (should be exactly
64 bytes)
* `public_key_file`: path to the Ed25519 public key (should be exactly
32 bytes)
* `domain`: SSO domain
* `allowed_services`: a list of regular expressions. A request will be
allowed only if the target SSO services matches one of these
expressions.
* `allowed_exchanges`: a list of regular expression source /
destination pairs (dictionaries with `src_regexp` and `dst_regexp`
attributes). Exchange requests will only be allowed if source and
destination SSO services both match one of these pairs.
* `allowed_cors_origins`: a list of "origins" (path-less URLs) for
CORS (Cross-Origin Resource Sharing) used to set
Access-Control-Allow-Origin headers; allows some sites to refresh
their SSO credentials on secondary or asynchronous requests
* `service_ttls`: a list of dictionaries used to set time-to-live for
SSO tickets for specific services. Each dictionary should have the
following attributes:
* `regexp`: regular expression that should match the SSO service
* `ttl`: TTL in seconds
* `auth_session_lifetime`: time-to-live (in seconds) for the
sso-server user authentication session. When it expires, the user
will have to login again.
* `session_secrets`: a list of two (or more, as long as the number is
even) secret keys to use for HTTP cookie-based sessions, in
*authentication-key*, *encryption-key* pairs. Authentication keys
can be 32 bytes (SHA128) or 64 bytes (SHA512), encryption keys
should be 16 (AES-128), 24 (AES-192) or 32 (AES-256) bytes long. For
key rotation, multiple pairs (old, new) can be specified so that
sessions are not immediately invalidated.
* `csrf_secret`: a secret key used for CSRF protection
* `auth_service`: the service name to use for the authentication
request sent to *auth-server* (generally "sso")
* `device_manager`: configuration for the device tracking module:
* `auth_key`: a long-term key to authenticate HTTP-based cookies
* `geo_ip_data_files`: GeoIP databases to use (in mmdb format), if
unset the module will use the default GeoLite2-Country db
* `keystore`: configures the connection to the keystore service
* `url`: URL for the keystore service
* `sharded`: if true, requests to the keystore service will be
partitioned according to the user's *shard* attribute
* `tls_config`: client TLS configuration
* `cert`: path to the client certificate
* `key`: path to the private key
* `ca`: path to the CA used to validate the server
* `keystore_enable_groups`: (a list) if set, the keystore will only be
enabled for users that are members of these groups
* `u2f_app_id`: set the U2F AppID - if unset, it will be autodetected
based on the domain name in the request
* `url_path_prefix`: URL path prefix of the SSO server application
(default /)
* `http_server`: specifies standard parameters for the HTTP server
* `tls`: server-side TLS configuration
* `cert`: path to the server certificate
* `key`: path to the server's private key
* `ca`: path to the CA used to validate clients; if set, clients
will be required to send a certificate (mTLS)
* `acl`: TLS-based access controls, a list of entries with the
following attributes:
* `path` is a regular expression to match the request URL path
* `cn` is a regular expression that must match the CommonName
part of the subject of the client certificate
* `trusted_forwarders`: list of trusted IP addresses (reverse
proxies). If a request comes from here, we will trust the
X-Forwarded-Proto and X-Real-IP headers when determining the
client IP address
* `max_inflight_requests`: maximum number of in-flight requests to
allow before server-side throttling kicks in
* `site_name`: sting to be used as site `<title>`.
* `site_logo`: path to an image to be used as logo, placed above the
modal form.
* `site_favicon`: path to a favicon.
## Device tracking
The idea is to track a small amount of non-personally-identifying data
for each device, and use it to notify users of unexpected
accesses. This information is tracked by the
[user-meta-server](https://git.autistici.org/id/usermetadb).
It is implemented very simply, with a long-term cookie stored in the
browser.
## Key store
On login, the login server can unlock the user's key store
(see [keystore](https://git.autistici.org/id/keystore)). The
associated key will be cleared either on logout, or when the login
session expires.
# SSO Proxy
The *sso-proxy* server adds SSO authentication and access controls to
unauthenticated backends (legacy applications, or apps that do not
support authentication altogether).
It is a straightforward reverse proxy that handles the SSO-related
methods directly and forwards everything else unchanged to the
backend. While it is possible to specify multiple backends for each
endpoint, the load balancing algorithm is extremely unsophisticated:
the proxy will simply pick a random backend on every request, without
any tracking of whether backends are up or not (this is obviously
improvable). Also note that the authenticated identity is **not**
passed along to the backend: since the backends are unauthenticated,
it wouldn't be safe for them to trust this information anyway, unless
they have a way to ensure it comes only from the trusted sso-proxy
(perhaps using TLS or other forms of transport verification). Finally,
*sso-proxy* only handles incoming requests based on their Host
attribute, not the request path. And the only access control rules
currently supported are group-based.
The proxy server has its own configuration file, */etc/sso/proxy.yml*
by default, which has the following attributes:
* `session_auth_key` and `session_enc_key` are secrets to be used for
HTTP-based sessions. For details on their syntax see the description
for `session_secrets` above.
* `sso_server_url` is the URL for the login server
* `sso_public_key_file` should point at a file containing the SSO
public key
* `sso_domain` is the SSO domain
* `backends` is the list of configured endpoints and associated
backends, each entry has the following attributes:
* `host` the HTTP host to serve
* `allowed_groups` is a list of the groups whose users will be
allowed access to the service
* `upstream` is a list of *host:port* addresses for the upstream
backends
* `tls_server_name` allows you to explicitly set the value of the
ServerName TLS extension on the outbound request. This is done do
de-couple the transport layer between proxy and backend from the
details of the actual HTTP request.
* `client_tls` specifies the client TLS configuration. If set, the
upstream request will use HTTPS, otherwise plain HTTP. Known
attributes:
* `cert`: path to the client certificate
* `key`: path to the private key
* `ca`: path to the CA used to validate the server
Given its characteristics, the proxy is currently best suited for
relatively low-volume, administrative applications, rather than for
user-visible services.
# API
The *sso-server* binary serves different types of HTTP traffic:
* the login/logout interface (user-facing)
* the SSO login endpoint (user-facing)
* the SSO ticket exchange endpoint (service-facing)
The ticket exchange API allows a service (the *source*) to exchange a
valid SSO ticket for itself with a SSO ticket, for the same user,
meant for a third-party service (*destination*). Its endpoint is
located at the URL `/exchange` and it accepts the following query
parameters:
* `cur_tkt`: valid source SSO ticket
* `cur_svc`: source SSO service
* `cur_nonce`: nonce for *cur_tkt*
* `new_svc`: destination SSO service
* `new_nonce`: nonce for the new SSO ticket
Note that annoyingly *cur_svc* and *cur_nonce* are redundant, as they
are already contained within *cur_tkt*, but the SSO ticket API won't
allow us to decode the ticket without verifying it at the same time.
The new ticket will not be valid any longer than the original one, or
the configured TTL for the new service, whichever comes first.
Group membership in the original ticket is passed along unchanged to
the new ticket.
# Implementation notes
The single-sign-on functionality works using HTTP cookies and
redirects between the protected service and the SSO server implemented
in this package. This part works without any Javascript, it's just
plain old HTTP (the browser must accept cookies though). SSO cookies
have a builtin (signed) expiration timestamp, and are set to be
automatically deleted on browser exit.
Logout, on the other hand, is more complex: in order to get the
browser to delete the cookies from the signed-in services, we use
XMLHttpRequests from the logout page, and expect the service logout
endpoints to support authenticated CORS. If Javascript is not
available, however, we try to clear the cookies using image requests,
but this may not work depending on the browser (Safari), or the
presence of privacy-protecting extensions meant to block third-party
cookies. In this case a message is displayed asking the user to quit
the browser, but this isn't really a satisfying solution.
package main
import (
"flag"
"io/ioutil"
"log"
"git.autistici.org/ai3/go-common/serverutil"
"gopkg.in/yaml.v2"
"git.autistici.org/id/go-sso/saml"
)
var (
addr = flag.String("addr", ":5007", "address to listen on")
configFile = flag.String("config", "/etc/sso/saml.yml", "`path` of config file")
)
// Config wraps together the standard HTTP server config and the SAML
// service configuration.
type Config struct {
SAMLConfig *saml.Config `yaml:"saml"`
ServerConfig *serverutil.ServerConfig `yaml:"http_server"`
}
func loadConfig() (*Config, error) {
// Read YAML config.
data, err := ioutil.ReadFile(*configFile)
if err != nil {
return nil, err
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
func main() {
log.SetFlags(0)
flag.Parse()
config, err := loadConfig()
if err != nil {
log.Fatalf("error loading configuration: %v", err)
}
s, err := saml.NewSAMLIDP(config.SAMLConfig)
if err != nil {
log.Fatalf("error instantiating SAML IDP: %v", err)
}
if err := serverutil.Serve(s, config.ServerConfig, *addr); err != nil {
log.Fatalf("error: %v", err)
}
}
package main
import (
"context"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"gopkg.in/yaml.v2"
"git.autistici.org/id/go-sso/proxy"
)
var (
addr = flag.String("addr", ":5003", "address to listen on")
configFile = flag.String("config", "/etc/sso/proxy.yml", "path of config file")
)
func loadConfig() (*proxy.Config, error) {
// Read YAML config.
data, err := ioutil.ReadFile(*configFile)
if err != nil {
return nil, err
}
var config proxy.Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
func main() {
log.SetFlags(0)
flag.Parse()
config, err := loadConfig()
if err != nil {
log.Fatal(err)
}
h, err := proxy.NewProxy(config)
if err != nil {
log.Fatal(err)
}
srv := &http.Server{
Addr: *addr,
Handler: h,
}
sigCh := make(chan os.Signal, 1)
go func() {
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
_ = srv.Close()
}()
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGINT)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
package main
import (
"flag"
"io/ioutil"
"log"
"git.autistici.org/ai3/go-common/serverutil"
"git.autistici.org/id/auth/client"
"gopkg.in/yaml.v2"
"git.autistici.org/id/go-sso/server"
)
var (
addr = flag.String("addr", ":4141", "tcp `address` to listen on")
configFile = flag.String("config", "/etc/sso/server.yml", "configuration `file`")
authSocket = flag.String("auth-socket", client.DefaultSocketPath, "authentication socket `path`")
)
// Config wraps together the sso-server configuration and the standard
// HTTP server config.
type Config struct {
server.Config `yaml:",inline"`
ServerConfig *serverutil.ServerConfig `yaml:"http_server"`
}
func loadConfig(path string) (*Config, error) {
// Read YAML config.
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}
func main() {
log.SetFlags(0)
flag.Parse()
config, err := loadConfig(*configFile)
if err != nil {
log.Fatal(err)
}
if err = config.Config.Compile(); err != nil {
log.Fatal(err)
}
loginService, err := server.NewLoginService(&config.Config)
if err != nil {
log.Fatal(err)
}
authClient := client.New(*authSocket)
httpSrv, err := server.New(loginService, authClient, &config.Config)
if err != nil {
log.Fatal(err)
}
if err := serverutil.Serve(httpSrv.Handler(), config.ServerConfig, *addr); err != nil {
log.Fatal(err)
}
}
package main
import (
"io/ioutil"
"os"
"testing"
)
var testConfig = `---
secret_key_file: "/etc/sso/secret.key"
public_key_file: "/etc/sso/public.key"
domain: "example.com"
allowed_services:
- "^(login|panel|monitor|logs)\\.example.com/$"
- "^\\d+\\.webmail\\.example.com/$"
allowed_exchanges:
- src_regexp: "^www.example.com/webmail/\\d+/$"
- dst_regexp: "^imap.example.com/$"
service_ttls:
- regexp: "^www.example.com/webmail/\\d+/$"
ttl: 43200
- regexp: "^imap.example.com/$"
ttl: 43200
- regexp: ".*"
ttl: 300
auth_session_lifetime: 43200
session_secrets:
- "iNQcyp5neUmbrxoj4yfRVhGL8HYGKNWRIv7t5ZiTxXwnJqBJYIU0gQx+1ar7Hsn0"
- "Xqphf9jjr/jZCk+m"
csrf_secret: "XLFtiymBU5p59K/IsqW/oh/5dfP4UC6JSNWMVeiQ8t8GjnB1rzusIFnyho5y4nE1"
auth_service: sso
device_manager:
auth_key: "ffolt81h4CA5kEcwckXmuUUkchwKQmRAeWb1H6Kpzx3+uGqwrVpBfGwzRSYaeir1"
`
func TestMain_LoadConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
ioutil.WriteFile(dir+"/config.yml", []byte(testConfig), 0640)
conf, err := loadConfig(dir + "/config.yml")
if err != nil {
t.Fatal("LoadConfig:", err)
}
if err := conf.Config.Compile(); err != nil {
t.Fatal("Compile:", err)
}
}
go-sso (0.2) unstable; urgency=medium
* Initial Release.
-- Autistici/Inventati <debian@autistici.org> Wed, 01 Nov 2017 10:37:36 +0000
Source: go-sso
Section: admin
Priority: optional
Maintainer: Autistici/Inventati <debian@autistici.org>
Build-Depends: debhelper (>=9), golang-any (>=1.11), dh-golang
Standards-Version: 3.9.6
Package: sso-server
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, auth-server
Description: Single-Sign-On server.
Single-Sign-On server, integrated with git.autistici.org/id/auth.
Package: sso-proxy
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Single-Sign-On HTTP proxy.
Single-Sign-On HTTP proxy.
Package: saml-server
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: SAML Single-Sign-On bridge
SAML Single-Sign-On bridge.
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: go-sso
Source: <https://git.autistici.org/id/go-sso>
Files: *
Copyright: 2017 Autistici/Inventati <info@autistici.org>
License: GPL-3.0+
License: GPL-3.0+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
.
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
#!/usr/bin/make -f
export DH_GOPKG = git.autistici.org/id/go-sso
export DH_GOLANG_EXCLUDES = vendor
%:
dh $@ --with systemd --with golang --buildsystem golang
override_dh_auto_install:
dh_auto_install -- --no-source
override_dh_systemd_enable:
dh_systemd_enable --no-enable
override_dh_systemd_start:
dh_systemd_start --no-start
#!/bin/sh
set -e
case "$1" in
configure)
addgroup --system --quiet saml-server
adduser --system --no-create-home --home /run/saml-server \
--disabled-password --disabled-login \
--quiet --ingroup saml-server saml-server
;;
esac
#DEBHELPER#
exit 0
[Unit]
Description=SAML SSO Bridge
[Service]
User=saml-server
Group=saml-server
EnvironmentFile=-/etc/default/saml-server
ExecStart=/usr/bin/saml-server --addr $ADDR
Restart=always
[Install]
WantedBy=multi-user.target
#!/bin/sh
set -e
case "$1" in
configure)
addgroup --system --quiet sso-proxy
adduser --system --no-create-home --home /run/sso-proxy \
--disabled-password --disabled-login \
--quiet --ingroup sso-proxy sso-proxy
;;
esac
#DEBHELPER#
exit 0
[Unit]
Description=SSO Proxy