From d347371c95ddc6409d6af6334f459fa75d859a11 Mon Sep 17 00:00:00 2001 From: Mark Bundschuh Date: Thu, 24 Oct 2024 16:57:39 -0400 Subject: [PATCH] initial commit --- .air.toml | 51 ++++++++++++++++++++++ .github/workflows/go.yaml | 29 +++++++++++++ .gitignore | 3 ++ .golangci.yml | 3 ++ .vscode/extensions.json | 5 +++ Dockerfile | 14 ++++++ Dockerfile.dev | 12 ++++++ LICENSE | 21 +++++++++ README.md | 54 +++++++++++++++++++++++ auth_provider.go | 64 +++++++++++++++++++++++++++ docker-compose-saml.yaml | 24 +++++++++++ docker-compose.yaml | 18 ++++++++ flake.lock | 61 ++++++++++++++++++++++++++ flake.nix | 28 ++++++++++++ go.mod | 24 +++++++++++ go.sum | 74 +++++++++++++++++++++++++++++++ main.go | 81 ++++++++++++++++++++++++++++++++++ saml_auth_provider.go | 91 +++++++++++++++++++++++++++++++++++++++ tls_certs.go | 86 ++++++++++++++++++++++++++++++++++++ 19 files changed, 743 insertions(+) create mode 100644 .air.toml create mode 100644 .github/workflows/go.yaml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .vscode/extensions.json create mode 100644 Dockerfile create mode 100644 Dockerfile.dev create mode 100644 LICENSE create mode 100644 README.md create mode 100644 auth_provider.go create mode 100644 docker-compose-saml.yaml create mode 100644 docker-compose.yaml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 saml_auth_provider.go create mode 100644 tls_certs.go diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..58fff2a --- /dev/null +++ b/.air.toml @@ -0,0 +1,51 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..1bb850d --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,29 @@ +name: Go +on: + pull_request: + push: + branches: + - master + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + + - name: Build + run: go build . + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + + - name: Lint + uses: golangci/golangci-lint-action@v6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa2b8dc --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/keys +/tmp +/tlskeys diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..4ad9dfc --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,3 @@ +linters: + enable: + - gofmt diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f3936b1 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "golang.go" + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5064f40 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.23.2-alpine +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY auth_provider.go main.go saml_auth_provider.go tls_certs.go /app +RUN go build -o main . + +FROM alpine:latest +WORKDIR /app +COPY --from=0 /app/main main +COPY keys/sp-cert.pem keys/sp-cert.pem +COPY keys/sp-key.pem keys/sp-key.pem +ENV ENV=prod +CMD ["/app/main"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..4f63f4d --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,12 @@ +FROM golang:1.23.2-alpine + +WORKDIR /app + +RUN go install github.com/air-verse/air@latest + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . /app + +CMD ["air", "-c", ".air.toml"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fcaa091 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Cyber Security Club @ tOSU + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ce674e --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Auth + +This is an auth platform and discord bot that interacts with SAML as a single binary, without Shibboleth or Nginx. + +## Development + +By default, the SAML provider is mocked out, so it is possible to develop without Service Provider secrets. Just run: + +``` +docker compose up --watch --build --remove-orphans +``` + +This will start a local server at http://localhost:3000 + +You will also want to install `gopls` for the language server in your IDE. + +## Development with SAML + +The certificate for our current credentials will expire `May 4 20:08:05 2031 GMT`, at which point it will need to be renewed. + +To develop with SAML credentials, put the Service Provider keys into `keys/sp-cert.pem` and `keys/sp-key.pem`. Then run: + +``` +docker compose -f docker-compose-saml.yaml up --watch --build --remove-orphans +``` + +This will start a local server at https://test-auth.osucyber.club. It will generate a self signed certificate for local https. + +## How does it work? + +Read this for specific details on OSU's SSO: https://webauth.service.ohio-state.edu/~shibboleth/index.html + +In summary: + +The OSU authentication system uses SAML2, like many other universities. This means that OSU runs an Identity Provider (IdP), which stores user data (like `Name.#`, email, and `BuckID`). Third parties can interact with the IdP by becoming a Service Provider (SP), think Schedule Planner. The OSU Cyber Security Club also has been granted private keys to be an SP at 2 hosts: `https://auth-test.osucyber.club` and `https://auth.osucyber.club`. We've configured DNS to point `auth-test.osucyber.club` to `127.0.0.1` for local testing, and it still requires self signed certificates. `https://auth.osucyber.club` is the public, production web server. + +Here are some other important facts about our setup in particular: +- Our registered entity ID is `https://auth.osucyber.club/shibboleth` +- Shibboleth assertions will be sent to our `/Shibboleth.sso/` and below (for example `/Shibboleth.sso/SAML2/POST`) + +The recommended way to actually use those keys as an SP is using [Shibboleth](https://shibboleth.net). Shibboleth was primarily developed in 2004 by an OSU employee and integrates nicely into Apache and IIS. But, it is kind of a nightmare. OSUCyber's former auth system, modelled after [sigpwny's auth system](https://github.com/sigpwny/sigpwny-shibboleth-auth) dockerized the process, which works this way: + +There are 2 containers side-by-side: +- Shibboleth + - Shibboleth's 3 processes, `shibd`, `shibauthorizer`, and `shibresponder` + - Configured with `OSU-attribute-policy.xml`, `OSU-metadata.cer`, `shibboleth2.xml`, `attribute-map.xml`, `sessionError.html` + - Nginx + - Using [a plugin to interact with Shibboleth](https://github.com/nginx-shib/nginx-http-shibboleth) + - Configured `/login` endpoint to be restricted by Shibboleth authentication, using redirects to sign in. Then once it is successful, it sets trusted HTTP headers like `Employeenumber` and `Displayname`. This is how the webapp will receive information about the OSU user. +- Webapp + - Whatever webapp you want to write + - Has a `/login` which reads from the trusted HTTP headers set by Nginx. Then the app can put it in its own database, or however it wants to handle it. + +However, updating any of the Shibboleth/Nginx stuff is really scary. Instead, it is possible to just use a SAML2 library to be an SP, all in one single place. That is what this repository is. We use [`crewjam/saml`](https://github.com/crewjam/saml) to handle being an SP in golang, and then build the rest of the auth app around it. It requires picking out only the important parts of the `shibboleth2.xml`, and makes significantly easier to read. diff --git a/auth_provider.go b/auth_provider.go new file mode 100644 index 0000000..113c6ee --- /dev/null +++ b/auth_provider.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "net/http" +) + +// Full list of attributes is here: +// https://webauth.service.ohio-state.edu/~shibboleth/user-attribute-reference.html (https://archive.is/H9bOB) +// +// Note that OSUCyber is only authorized for a subset of these attributes. As of 2024-10-23, this is: +// sn, IDMUID, displayName, eduPersonScopedAffiliation, employeeNumber, givenName, mail, SessionIndex, eduPersonPrincipalName +type OSUAttributes struct { + // This attribute is `sn` in the OSU Shibboleth user attribute reference. + Surname string + // This attribute is `IDM ID` in the OSU Shibboleth user attribute reference. + IDMUID string + // This attribute is `displayName` in the OSU Shibboleth user attribute reference. + DisplayName string + // This attribute is `eduPersonScopedAffiliation` in the OSU Shibboleth user attribute reference. + Affiliations []string + // This attribute is `employeeNumber` in the OSU Shibboleth user attribute reference. + BuckID string + // This attribute is `givenName` in the OSU Shibboleth user attribute reference. + GivenName string + // This attribute is `mail` in the OSU Shibboleth user attribute reference. + Email string + // This attribute is `SessionIndex` in the OSU Shibboleth user attribute reference. + SessionIndex string +} + +type AuthProvider interface { + attributesFromContext(ctx context.Context) *OSUAttributes + requireAuth(handler http.Handler) http.Handler + globalLogout(w http.ResponseWriter, r *http.Request) +} + +type MockAuthProvider struct{} + +func (m MockAuthProvider) attributesFromContext(ctx context.Context) *OSUAttributes { + return &OSUAttributes{ + GivenName: "Brutus", + Surname: "Buckeye", + DisplayName: "Brutus Buckeye", + BuckID: "500123456", + IDMUID: "IDM123456789", + Email: "buckeye.1@osu.edu", + Affiliations: []string{"member@osu.edu", "student@osu.edu"}, + SessionIndex: "_0123456789abcdef01234566890abcde", + } +} + +func (m MockAuthProvider) requireAuth(handler http.Handler) http.Handler { + return handler +} + +func (m MockAuthProvider) globalLogout(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Location", "https://webauth.service.ohio-state.edu/cgi-bin/logout.cgi") + w.WriteHeader(http.StatusFound) +} + +func mockAuthProvider() *MockAuthProvider { + return &MockAuthProvider{} +} diff --git a/docker-compose-saml.yaml b/docker-compose-saml.yaml new file mode 100644 index 0000000..c85faee --- /dev/null +++ b/docker-compose-saml.yaml @@ -0,0 +1,24 @@ +services: + auth: + ports: + - 443:443 + build: + dockerfile: Dockerfile.dev + command: air + volumes: + - ./keys:/app/keys + - ./tlskeys:/app/tlskeys + environment: + - ENV=saml + develop: + watch: + - action: sync + path: . + target: /app + ignore: + - tlskeys + - .git/ + - action: rebuild + path: go.mod + - action: rebuild + path: go.sum diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..b1febd4 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,18 @@ +services: + auth: + ports: + - 3000:3000 + build: + dockerfile: Dockerfile.dev + command: air + develop: + watch: + - action: sync + path: . + target: /app + ignore: + - .git/ + - action: rebuild + path: go.mod + - action: rebuild + path: go.sum diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..428e19b --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1728979988, + "narHash": "sha256-GBJRnbFLDg0y7ridWJHAP4Nn7oss50/VNgqoXaf/RVk=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "7881fbfd2e3ed1dfa315fca889b2cfd94be39337", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "utils": "utils" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d61bcbb --- /dev/null +++ b/flake.nix @@ -0,0 +1,28 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + utils.url = "github:numtide/flake-utils"; + }; + + outputs = { + nixpkgs, + utils, + ... + }: + utils.lib.eachDefaultSystem ( + system: let + pkgs = import nixpkgs { + inherit system; + }; + in rec { + devShells.default = pkgs.mkShell { + name = "auth"; + packages = with pkgs; [ + go + gopls + air + ]; + }; + } + ); +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eae4b16 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module main + +go 1.23.2 + +require github.com/k0kubun/pp/v3 v3.2.0 + +require ( + github.com/crewjam/httperr v0.2.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect +) + +require ( + github.com/beevik/etree v1.4.1 // indirect + github.com/crewjam/saml v0.4.14 + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect + github.com/russellhaering/goxmldsig v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2295351 --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/beevik/etree v1.4.1 h1:PmQJDDYahBGNKDcpdX8uPy1xRCwoCGVUiW669MEirVI= +github.com/beevik/etree v1.4.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo= +github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4= +github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c= +github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= +github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= +github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/k0kubun/pp/v3 v3.2.0 h1:h33hNTZ9nVFNP3u2Fsgz8JXiF5JINoZfFq4SvKJwNcs= +github.com/k0kubun/pp/v3 v3.2.0/go.mod h1:ODtJQbQcIRfAD3N+theGCV1m/CBxweERz2dapdz1EwA= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU= +github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM= +github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys= +github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..f3bb391 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "crypto/tls" + "fmt" + "log" + "net/http" + "net/url" + "os" + "time" + + "github.com/k0kubun/pp/v3" +) + +func hello(s AuthProvider) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + fmt.Println("/hello") + + attributes := s.attributesFromContext(r.Context()) + pp.Println(attributes) + + fmt.Fprintf(w, "Hello, %s!", attributes.GivenName) + } +} + +func main() { + mux := http.NewServeMux() + + authEnvironment := os.Getenv("ENV") + var authProvider AuthProvider + + if authEnvironment == "" { + authProvider = mockAuthProvider() + } else { + rootURL, _ := url.Parse("https://auth.osucyber.club") + if authEnvironment == "saml" { + rootURL, _ = url.Parse("https://auth-test.osucyber.club") + } + + keyPair, err := tls.LoadX509KeyPair("keys/sp-cert.pem", "keys/sp-key.pem") + if err != nil { + panic(err) + } + + authProvider, _ = samlAuthProvider(mux, rootURL, &keyPair) + } + + mux.Handle("/hello", authProvider.requireAuth(http.HandlerFunc(hello(authProvider)))) + mux.Handle("/logout", authProvider.requireAuth(http.HandlerFunc(authProvider.globalLogout))) + + if authEnvironment == "saml" { + log.Println("Starting server on :443. Visit https://auth-test.osucyber.club and accept the self-signed certificate") + keyPair, err := getTlsCert() + if err != nil { + panic(err) + } + server := &http.Server{ + Addr: ":443", + ReadHeaderTimeout: time.Second * 10, + Handler: mux, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{*keyPair}, + }, + } + _ = server.ListenAndServeTLS("", "") + } else { + if authEnvironment == "" { + log.Println("Starting server on :3000. Visit http://localhost:3000") + } else { + log.Println("Starting server on :3000") + } + + server := &http.Server{ + Addr: ":3000", + ReadHeaderTimeout: time.Second * 10, + Handler: mux, + } + _ = server.ListenAndServe() + } +} diff --git a/saml_auth_provider.go b/saml_auth_provider.go new file mode 100644 index 0000000..cceaeec --- /dev/null +++ b/saml_auth_provider.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "log" + "net/http" + "net/url" + + "github.com/crewjam/saml/samlsp" +) + +type SamlAuthProvider struct { + samlSP *samlsp.Middleware +} + +func (s SamlAuthProvider) attributesFromContext(ctx context.Context) *OSUAttributes { + session := samlsp.SessionFromContext(ctx).(samlsp.SessionWithAttributes) + attributes := session.GetAttributes() + + return &OSUAttributes{ + Surname: attributes.Get("sn"), + IDMUID: attributes.Get("IDMUID"), + DisplayName: attributes.Get("displayName"), + Affiliations: attributes["eduPersonScopedAffiliation"], + BuckID: attributes.Get("employeeNumber"), + GivenName: attributes.Get("givenName"), + Email: attributes.Get("mail"), + SessionIndex: attributes.Get("SessionIndex"), + } +} + +func (s SamlAuthProvider) requireAuth(handler http.Handler) http.Handler { + return s.samlSP.RequireAccount(handler) +} + +func (s *SamlAuthProvider) globalLogout(w http.ResponseWriter, r *http.Request) { + err := s.samlSP.Session.DeleteSession(w, r) + if err != nil { + panic(err) // TODO handle error + } + + w.Header().Add("Location", "https://webauth.service.ohio-state.edu/cgi-bin/logout.cgi") + w.WriteHeader(http.StatusFound) +} + +func samlAuthProvider(mux *http.ServeMux, rootURL *url.URL, keyPair *tls.Certificate) (*SamlAuthProvider, error) { + var err error + + keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) + if err != nil { + return nil, err + } + + idpMetadataURL, err := url.Parse("https://webauth.service.ohio-state.edu/OSU-idp-metadata.xml") + if err != nil { + return nil, err + } + idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient, *idpMetadataURL) + if err != nil { + return nil, err + } + + samlSP, err := samlsp.New(samlsp.Options{ + URL: *rootURL, + EntityID: "https://auth.osucyber.club/shibboleth", + Key: keyPair.PrivateKey.(*rsa.PrivateKey), + Certificate: keyPair.Leaf, + IDPMetadata: idpMetadata, + CookieName: "_saml", + }) + if err != nil { + return nil, err + } + + acsUrl := *rootURL + acsUrl.Path = "/Shibboleth.sso/SAML2/POST" + samlSP.ServiceProvider.AcsURL = acsUrl + samlSP.OnError = func(w http.ResponseWriter, r *http.Request, err error) { + log.Println("SAML error:", err) + } + + mux.Handle("/Shibboleth.sso/", samlSP) + mux.Handle("/metadata.xml", http.HandlerFunc(samlSP.ServeMetadata)) + + return &SamlAuthProvider{ + samlSP, + }, nil +} diff --git a/tls_certs.go b/tls_certs.go new file mode 100644 index 0000000..6f7dd88 --- /dev/null +++ b/tls_certs.go @@ -0,0 +1,86 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "time" +) + +func getTlsCert() (*tls.Certificate, error) { + _, certExists := os.Stat("tlskeys/auth-test-osucyber-club-selfsigned-cert.pem") + _, keyExists := os.Stat("tlskeys/auth-test-osucyber-club-selfsigned-key.pem") + + if certExists == nil && keyExists == nil { + cert, err := tls.LoadX509KeyPair("tlskeys/auth-test-osucyber-club-selfsigned-cert.pem", "tlskeys/auth-test-osucyber-club-selfsigned-key.pem") + if err != nil { + return nil, err + } + + return &cert, nil + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 64)) + + now := time.Now() + certTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "auth-test.osucyber.club", + Country: []string{"US"}, + Organization: []string{"Cyber Security Club @ The Ohio State University"}, + Locality: []string{"Columbus"}, + Province: []string{"OH"}, + OrganizationalUnit: []string{"auth-autogenerated"}, + }, + NotBefore: now, + NotAfter: now.AddDate(1, 0, 0), // Valid for 1 year + DNSNames: []string{"auth-test.osucyber.club"}, + EmailAddresses: []string{"info@osucyber.club"}, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + certBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &privateKey.PublicKey, privateKey) + if err != nil { + return nil, err + } + + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }) + + if err := os.WriteFile("tlskeys/auth-test-osucyber-club-selfsigned-key.pem", privateKeyPEM, 0600); err != nil { + return nil, err + } + + certificatePEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + if err := os.WriteFile("tlskeys/auth-test-osucyber-club-selfsigned-cert.pem", certificatePEM, 0644); err != nil { + return nil, err + } + + tlsCert, err := tls.X509KeyPair(certificatePEM, privateKeyPEM) + if err != nil { + return nil, err + } + + return &tlsCert, nil +}