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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ 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}"
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"

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

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 /
Expand All @@ -30,4 +30,5 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /src/templates /templates/

USER vpn-webauth

ENTRYPOINT ["/vpn-webauth"]
204 changes: 110 additions & 94 deletions README.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions build_hashes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash

CSS="sha384-$(cat templates/assets/css.css | openssl dgst -sha384 -binary | openssl base64 -A)"
FONT_AWESOME="sha384-$(cat templates/assets/font-awesome.min-4.7.0.css | openssl dgst -sha384 -binary | openssl base64 -A)"
JQUERY="sha384-$(cat templates/assets/jquery-3.5.1.min.js | openssl dgst -sha384 -binary | openssl base64 -A)"
MATERIAL="sha384-$(cat templates/assets/material-icons.css | openssl dgst -sha384 -binary | openssl base64 -A)"
MATERIALIZE_JS="sha384-$(cat templates/assets/materialize.min-0.97.5.js | openssl dgst -sha384 -binary | openssl base64 -A)"
MATERIALIZE_CSS="sha384-$(cat templates/assets/materialize-0.97.5.min.css | openssl dgst -sha384 -binary | openssl base64 -A)"
SCRIPT="sha384-$(cat templates/assets/script.js | openssl dgst -sha384 -binary | openssl base64 -A)"

sed -i "s@.*<meta.*@<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' '$CSS' '$FONT_AWESOME' '$MATERIALIZE_CSS' '$MATERIAL'; img-src 'self' {{.LogoURL}}; script-src 'self' '$JQUERY' '$MATERIALIZE_JS' '$SCRIPT'; form-action 'self'\">@g" templates/header.html
sed -i "s@.*material-icons.css.*@<link href=\"/assets/material-icons.css\" rel=\"stylesheet\" integrity=\"$MATERIAL\">@g" templates/header.html
sed -i "s@.*materialize-0.97.5.min.css.*@<link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/materialize-0.97.5.min.css\" integrity=\"$MATERIALIZE_CSS\">@g" templates/header.html
sed -i "s@.*font-awesome.min-4.7.0.css.*@<link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/font-awesome.min-4.7.0.css\" integrity=\"$FONT_AWESOME\">@g" templates/header.html
sed -i "s@.*css.css.*@<link rel=\"stylesheet\" type=\"text/css\" href=\"/assets/css.css\" integrity=\"$CSS\">@g" templates/header.html
sed -i "s@.*jquery-3.5.1.min.js.*@<script type=\"text/javascript\" src=\"/assets/jquery-3.5.1.min.js\" integrity=\"$JQUERY\"></script>@g" templates/header.html
sed -i "s@.*materialize.min-0.97.5.js.*@<script type=\"text/javascript\" src=\"/assets/materialize.min-0.97.5.js\" integrity=\"$MATERIALIZE_JS\"></script>@g" templates/header.html
sed -i "s@.*script.js.*@<script type=\"text/javascript\" src=\"/assets/script.js\" integrity=\"$SCRIPT\"></script>@g" templates/header.html
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
2 changes: 1 addition & 1 deletion pkged.go

Large diffs are not rendered by default.

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;
}
40 changes: 37 additions & 3 deletions templates/assets/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ $(document).ready(async function(){
});
$("#data-session-validity").text(new Date(userInfo.SessionExpiry * 1000).toLocaleString());

if ('permissions' in navigator) {
if ('permissions' in navigator && !("ontouchstart" in document.documentElement)) {
const notificationPerm = await navigator.permissions.query({name:'notifications'});
console.log(`Notifications are ${notificationPerm.state}`);
if (notificationPerm.state === "granted") {
Expand All @@ -497,7 +497,7 @@ $(document).ready(async function(){

// If notifications are enabled and the user allowed them, enable either
// Service Worker or SSE.
if (userInfo.EnableNotifications) {
if (userInfo.EnableNotifications && !("ontouchstart" in document.documentElement)) {
console.log(`Notification.permission=${Notification.permission}`);
const hasWorkerPush = checkWorkerPush();
if (Notification.permission === "default") {
Expand Down Expand Up @@ -558,5 +558,39 @@ $(document).ready(async function(){
$(this).val("");
}
}).change();


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();
}
}

const otcHander = async function() {
const dataLength = $('#otc').val().length;
if(dataLength > 0) {
$("#error").hide();
}
if (dataLength == 6) {
await validateOneTimePass(true, $('#otc').val());
$('#otc').val("");
} else {
$("#error").show();
}
}

if(document.getElementById('otp-button')) {
$('#otp-button').on('click', otpHandler);
document.getElementById('otp-button').addEventListener('touchstart', otpHandler);
}
if(document.getElementById('otc-button')) {
$('#otc-button').on('click', otcHander);
document.getElementById('otc-button').addEventListener('touchstart', otcHander);
}
});
5 changes: 2 additions & 3 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>
</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>
</div>
<br/><br/>
</div>
Expand All @@ -73,7 +75,4 @@ <h2>Confirm you're you!</h2>
</center>

</main>
</body>

</html>
{{ end }}
Binary file added templates/fonts/FontAwesome.otf
Binary file not shown.
Binary file added templates/fonts/fontawesome-webfont.eot
Binary file not shown.
Loading