Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Generic OAuth2 Provider Support #18

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
23 changes: 1 addition & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,33 +1,12 @@
FROM golang:alpine AS builder
WORKDIR /src

ENV USER=vpn-webauth
ENV UID=10001

COPY go.mod .
COPY go.sum .
RUN go mod download

RUN apk --update --upgrade --no-cache add git gcc g++ ca-certificates && update-ca-certificates
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
ethantmcgee marked this conversation as resolved.
Show resolved Hide resolved

COPY . .
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o /out/vpn-webauth .


FROM alpine AS bin
ethantmcgee marked this conversation as resolved.
Show resolved Hide resolved
COPY --from=builder /out/vpn-webauth /
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /src/templates /templates/

USER vpn-webauth
ENTRYPOINT ["/vpn-webauth"]
ENTRYPOINT ["go", "run", "server.go", "main.go"]
ethantmcgee marked this conversation as resolved.
Show resolved Hide resolved
204 changes: 110 additions & 94 deletions README.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions controllers/oauth2/oauth2_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func New(db *gorm.DB, config *models.Config) *OAuth2Controller {

} else if config.OAuth2Provider == "azure" {
oAuthProvider = services.NewMicrosoftProvider(config.RedirectDomain.String(), "", config.OAuth2ClientID, config.OAuth2ClientSecret)

} else if config.OAuth2Provider == "generic" {
oAuthProvider = services.NewGenericProvider(config.RedirectDomain.String(), config.OAuth2TokenURL, config.OAuth2AuthorizeURL, config.OAuth2InfoURL, config.OAuth2ClientID, config.OAuth2ClientSecret)
}

return &OAuth2Controller{db: db, config: config}
Expand Down
40 changes: 40 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version: '3.7'

services:
db:
image: postgres:14
environment:
POSTGRES_PASSWORD: password
restart: unless-stopped
volumes:
- datavolume:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
app:
build: .
ports:
- 8080:8080
depends_on:
db:
condition: service_healthy
environment:
HOST: "0.0.0.0"
ENFORCEMFA: "true"
ENCRYPTIONKEY: "changeme"
DBTYPE: "postgres"
DBDSN: "host=db user=postgres password=password database=postgres port=5432"
ENFORCEMFA: "true"
VPNCHECKPASSWORD: "changeme"
OAUTH2PROVIDER: "changeme"
OAUTH2CLIENTID: "changeme"
OAUTH2CLIENTSECRET: "changeme"
REDIRECTDOMAIN: "https://vpn.mycompany.com"
ADMINEMAIL: "[email protected]"
VAPIDPUBLICKEY: "changeme"
VAPIDPRIVATEKEY: "changeme"
restart: unless-stopped
volumes:
datavolume:
21 changes: 17 additions & 4 deletions models/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type Config struct {
OAuth2ClientSecret string // OAUTH2CLIENTSECRET
OAuth2Provider string // OAUTH2PROVIDER
OAuth2Tenant string // OAUTH2TENANT
OAuth2TokenURL string // OAUTH2TOKENURL
OAuth2AuthorizeURL string // OAUTH2AUTHORIZEURL
OAuth2InfoURL string // OAUTH2INFOURL
EnableNotifications bool // ENABLENOTIFICATIONS
EnforceMFA bool // ENFORCEMFA
MaxBodySize int64 // not documented
Expand All @@ -50,6 +53,7 @@ type Config struct {
VPNSessionValidity time.Duration // VPNSESSIONVALIDITY
WebSessionValidity time.Duration // WEBSESSIONVALIDITY
WebSessionProofTimeout time.Duration // WEBSESSIONPROOFTIMEOUT
NonceCode string
}

func (config *Config) New() Config {
Expand Down Expand Up @@ -91,18 +95,27 @@ func (config *Config) New() Config {
func (config *Config) Verify() {
log.Printf("VPN Session validity set to %v", config.VPNSessionValidity)
log.Printf("Web Session validity set to %v", config.WebSessionValidity)
log.Printf("Google callback redirect set to %s", config.RedirectDomain)
log.Printf("Callback redirect set to %s", config.RedirectDomain)
if config.OAuth2Provider == "" {
log.Fatal("OAUTH2PROVIDER is not set, must be either google or azure")
log.Fatal("OAUTH2PROVIDER is not set, must be either google, azure, or generic")
} else {
config.OAuth2Provider = strings.ToLower(config.OAuth2Provider)
if config.OAuth2Provider != "google" && config.OAuth2Provider != "azure" {
log.Fatal("OAUTH2PROVIDER is invalid, must be either google or azure")
if config.OAuth2Provider != "google" && config.OAuth2Provider != "azure" && config.OAuth2Provider != "generic" {
log.Fatal("OAUTH2PROVIDER is invalid, must be either google, azure, or generic")
}
}
if config.OAuth2Provider == "azure" && config.OAuth2Tenant == "" {
log.Fatal("Microsoft/Azure OAuth2 provider requires OAUTH2TENANT to be set")
}
if config.OAuth2Provider == "generic" && config.OAuth2TokenURL == "" {
log.Fatal("Generic OAuth2 provider requires OAUTH2TOKENURL to be set")
}
if config.OAuth2Provider == "generic" && config.OAuth2AuthorizeURL == "" {
log.Fatal("Generic OAuth2 provider requires OAUTH2AUTHORIZEURL to be set")
}
if config.OAuth2Provider == "generic" && config.OAuth2InfoURL == "" {
log.Fatal("Generic OAuth2 provider requires OAUTH2INFOURL to be set")
}
if config.OAuth2ClientID == "" {
log.Fatal("OAUTH2CLIENTID is not set")
}
Expand Down
14 changes: 14 additions & 0 deletions routes/template_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"net/http"
"os"
"strings"
"math/rand"
"fmt"

"github.com/m-barthelemy/vpn-webauth/models"
"github.com/markbates/pkger"
Expand All @@ -20,6 +22,16 @@ var config models.Config
var templates *template.Template
var assets map[string]string

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

func NewTemplateHandler(config *models.Config) *TemplateHandler {
assets = make(map[string]string)
return &TemplateHandler{config: config}
Expand All @@ -36,6 +48,8 @@ func (g *TemplateHandler) HandleEmbeddedTemplate(response http.ResponseWriter, r
fileName = "index"
}

g.config.NonceCode = RandStringBytes(32)
fmt.Println(g.config.NonceCode)
err := templates.ExecuteTemplate(response, fileName, g.config)
if err != nil {
log.Printf("Error serving template %s: %s", fileName, err.Error())
Expand Down
78 changes: 76 additions & 2 deletions services/oauth2_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import(
"fmt"
"io/ioutil"
"net/http"
"net/url"
"context"
"strings"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/microsoft"
Expand All @@ -15,7 +17,11 @@ import(
type OAuth2User struct {
Id string `json:"sub"`
Email string `json:"email"`
EmailVerified string `json:"email_verified"`
EmailVerified bool `json:"email_verified"`
ethantmcgee marked this conversation as resolved.
Show resolved Hide resolved
}

type OAuth2Token struct {
AccessToken string `json:"access_token"`
}

type OAuth2Provider interface {
Expand Down Expand Up @@ -117,4 +123,72 @@ func (p *MicrosoftProvider) GetUserInfo(code string) (OAuth2User, error) {

err = json.Unmarshal(contents, &user)
return user, err
}
}

// Generic OAuth

type GenericProvider struct {
oAuthConfig *oauth2.Config
authorizeUrl string
tokenUrl string
userInfoUrl string
}

func NewGenericProvider(redirectDomain string, tokenUrl string, authorizeUrl string, userInfoUrl string, clientID string, clientSecret string) *GenericProvider {
p := GenericProvider{}
p.oAuthConfig = &oauth2.Config{
RedirectURL: fmt.Sprintf("%s/auth/generic/callback", redirectDomain),
ClientID: clientID,
ClientSecret: clientSecret,
}
p.authorizeUrl = authorizeUrl
p.tokenUrl = tokenUrl
p.userInfoUrl = userInfoUrl
return &p
}

func (p *GenericProvider) GetURL(state string) string {
return fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=openid+email&state=%s", p.authorizeUrl, p.oAuthConfig.ClientID, url.QueryEscape(p.oAuthConfig.RedirectURL), state)
}

func (p *GenericProvider) GetUserInfo(code string) (OAuth2User, error) {
var token OAuth2Token
var user OAuth2User

data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("redirect_uri", p.oAuthConfig.RedirectURL)
data.Set("code", code)
data.Set("client_id", p.oAuthConfig.ClientID)
data.Set("client_secret", p.oAuthConfig.ClientSecret)

client := http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodPost, p.tokenUrl, strings.NewReader(data.Encode())) // URL-encoded payload
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
response, err := client.Do(req)
if err != nil {
return user, fmt.Errorf("OAuth2Controller: failed to get token: %s", err.Error())
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
return user, fmt.Errorf("OAuth2Controller: failed to read token response: %s", err.Error())
}
err = json.Unmarshal(contents, &token)

client = http.Client{Timeout: 10 * time.Second}
req, err = http.NewRequest("GET", p.userInfoUrl, nil)
req.Header.Set("authorization", "Bearer " + token.AccessToken)
response, err = client.Do(req)
if err != nil {
return user, fmt.Errorf("OAuth2Controller: failed to get user info: %s", err.Error())
}
defer response.Body.Close()
contents, err = ioutil.ReadAll(response.Body)
if err != nil {
return user, fmt.Errorf("OAuth2Controller: failed to read userinfo response: %s", err.Error())
}
err = json.Unmarshal(contents, &user)

return user, err
}
2 changes: 1 addition & 1 deletion templates/addDevice.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ <h4 class="section-title">Add new Device or new Browser</h4>
<div class="row">
<div class="col s12">
<a id="register-otc" class="btn-large waves-effect blue-grey left mfa-choose-btn">
<i class="large material-icons left" style="font-size: 2.2em;">lock_open</i>&nbsp;Get a single usage code
<i class="large material-icons left add-device">lock_open</i>&nbsp;Get a single usage code
</a>
</div>
<br/>
Expand Down
4 changes: 4 additions & 0 deletions templates/assets/css.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,8 @@ main {

.white {
color: #ffffff;
}

.add-device {
font-size: 2.2em;
}
1 change: 0 additions & 1 deletion templates/assets/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -558,5 +558,4 @@ $(document).ready(async function(){
$(this).val("");
}
}).change();

});
20 changes: 20 additions & 0 deletions templates/enter2fa.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ <h2>Confirm you're you!</h2>
<i class="material-icons prefix">keyboard</i>
<input class='validate otp' type='number' id='otc' max="999999" minlength="6" maxlength="6"/>
<span class="helper-text" data-error="Must be 6 digits" data-success="right"></span>
<button type="button" class="waves-effect waves-light btn" id="otc-button">Submit</button>
ethantmcgee marked this conversation as resolved.
Show resolved Hide resolved
</div>
<br/><br/>
</div>
Expand Down Expand Up @@ -55,6 +56,7 @@ <h2>Confirm you're you!</h2>
<input class='validate otp' type='number' id='otp' max="999999" minlength="6" maxlength="6" autofocus/>
<label for='otp'>Use the entry starting with "<span name="data-connection-name"></span>"</label>
<span class="helper-text" data-error="Must be 6 digits" data-success="right"></span>
<button type="button" class="waves-effect waves-light btn" id="otp-button">Submit</button>
ethantmcgee marked this conversation as resolved.
Show resolved Hide resolved
</div>
<br/><br/>
</div>
Expand All @@ -73,6 +75,24 @@ <h2>Confirm you're you!</h2>
</center>

</main>
<script nonce="{{.NonceCode}}">
const otpHandler = async function() {
const dataLength = $('#otp').val().length;
if(dataLength > 0) {
$("#error").hide();
}
if (dataLength == 6) {
await validateOneTimePass(false, $('#otp').val());
$('#otp').val("");
} else {
$("#error").show();
}
}

document.getElementById('otp-button').addEventListener('click', otpHandler);

document.getElementById('otp-button').addEventListener('touchstart', otpHandler);
ethantmcgee marked this conversation as resolved.
Show resolved Hide resolved
</script>
</body>

</html>
Expand Down
17 changes: 9 additions & 8 deletions templates/header.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{{ define "header" }}
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; object-src 'none'; media-src 'none'; connect-src 'self'; font-src https://fonts.gstatic.com 'self'; child-src 'self'; style-src 'self'; img-src 'self' {{.LogoURL}}; script-src 'self'; form-action 'self';">
<link href="/assets/material-icons.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="/assets/materialize-0.97.5.min.css">
<link rel="stylesheet" type="text/css" href="/assets/font-awesome.min-4.7.0.css">
<link rel="stylesheet" type="text/css" href="/assets/css.css">
<script type="text/javascript" src="/assets/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="/assets/materialize.min-0.97.5.js"></script>
<script type="text/javascript" src="/assets/script.js"></script>
content="default-src 'self'; object-src 'none'; media-src 'none'; connect-src 'self'; font-src https://fonts.gstatic.com 'self'; child-src 'self'; style-src 'self' 'sha384-Tn+eHgvLDlHfZ/Bd0HmrFRKnaocbJJECUsEAjjg2gey5liDBv1trMEyh2l7XC2C+' 'sha384-1ji7hb9jc+M2e4aPgCIK93lON9Hxx38Vo/3oNk9vrJsU8JbrdFdLs+VmVE1YNiuM' 'sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN' 'sha384-TeXUgdWM9e/rDL9CedCXsh4Z0a6aZJUiyD7Vfz3S+H1REIOyQEXhpneyhHZswE3a' 'nonce-{{.NonceCode}}'; img-src 'self' {{.LogoURL}}; script-src 'self' 'sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2' 'sha384-jnt1QI5LA9Z8CEqFV7YvpkT/kvVzzSDZbit0VjFaNiz/XtzoN8OA7z/RI/cbzs95' 'sha384-lCnwfMpb5VT9dtF36tAIDhczx0j9zjzzpFY7qIxdwU+0VPRYT7xtOTsVIVkcwbhm' 'nonce-{{.NonceCode}}'; form-action 'self'">

<link href="/assets/material-icons.css" rel="stylesheet" integrity="sha384-Tn+eHgvLDlHfZ/Bd0HmrFRKnaocbJJECUsEAjjg2gey5liDBv1trMEyh2l7XC2C+">
<link rel="stylesheet" type="text/css" href="/assets/materialize-0.97.5.min.css" integrity="sha384-1ji7hb9jc+M2e4aPgCIK93lON9Hxx38Vo/3oNk9vrJsU8JbrdFdLs+VmVE1YNiuM">
<link rel="stylesheet" type="text/css" href="/assets/font-awesome.min-4.7.0.css" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN">
<link rel="stylesheet" type="text/css" href="/assets/css.css" integrity="sha384-TeXUgdWM9e/rDL9CedCXsh4Z0a6aZJUiyD7Vfz3S+H1REIOyQEXhpneyhHZswE3a">
<script type="text/javascript" src="/assets/jquery-3.5.1.min.js" integrity="sha384-ZvpUoO/+PpLXR1lu4jmpXWu80pZlYUAfxl5NsBMWOEPSjUn/6Z/hRTt8+pR6L4N2"></script>
<script type="text/javascript" src="/assets/materialize.min-0.97.5.js" integrity="sha384-jnt1QI5LA9Z8CEqFV7YvpkT/kvVzzSDZbit0VjFaNiz/XtzoN8OA7z/RI/cbzs95"></script>
<script type="text/javascript" src="/assets/script.js" integrity="sha384-lCnwfMpb5VT9dtF36tAIDhczx0j9zjzzpFY7qIxdwU+0VPRYT7xtOTsVIVkcwbhm"></script>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of having these integrity checksums! I'm not too familiar with that, is there a way these could be generated automatically, for example during the build or during the stage when the assets are embedded into the binary? That would make further updates to the assets easier.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure, but it should be do-able. Mobile browsers complain quit a bit without those is the reason they were added.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I have added a script for the integrity checksums. And mobile safari seems happy. I'm not sure how you want to integrate the script into the build pipeline

</head>
{{ end }}
Loading