Skip to content

Instantly share code, notes, and snippets.

@shanna
Last active April 19, 2022 18:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shanna/2139479a94d2cb3cb1227d7b91c455df to your computer and use it in GitHub Desktop.
Save shanna/2139479a94d2cb3cb1227d7b91c455df to your computer and use it in GitHub Desktop.
Wails OIDC OAuth2 minimal inline hack.
/*
Minimal Wails OIDC in-window proof of concept.
*/
package main
import (
"context"
"fmt"
"net/http"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/grokify/go-pkce"
"golang.org/x/oauth2"
)
// Wails V2 application URI.
const FrontendURI = "wails://wails/"
const OAuth2Issuer = "https://accounts.google.com"
// The Go HTTP endpoint we'll spin up to handle oauth2 redirects.
//
// In a real app you'd want to use an ephemeral port.
var OAuth2RedirectURL = "http://localhost:9991/auth/callback"
type Claims struct {
Email string `json:"email"`
Verified bool `json:"email_verified"`
}
// App struct
type App struct {
ctx context.Context
provider *oidc.Provider
auth *oauth2.Config
claims *Claims
verifier string
}
func NewApp() *App {
verifier := pkce.NewCodeVerifier()
// OIDC though you can just create the URLs yourself.
provider, _ := oidc.NewProvider(context.Background(), OAuth2Issuer)
// I don't get why bug Google seems to require a secret with PKCE still unless I missing something in my
// implementation?
//
// Google generates a secret for web app clients still and I don't seem to be able to omit it or provide a dummy
// value. Looking around Stack Overflow I see a lot of confusion.
auth := &oauth2.Config{
Endpoint: provider.Endpoint(),
ClientID: "client-id",
ClientSecret: "not-so-secret-secret",
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
RedirectURL: OAuth2RedirectURL,
}
app := &App{
provider: provider,
auth: auth,
verifier: verifier, // TODO: Needs storage if you don't want to log in each app-start.
}
return app
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// TODO: The handler path needs to be whatever you set up on the OIDC provider.
http.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
// TODO: Emit events for auth errors and success?
// Check errors on this lot obv:
token, _ := a.auth.Exchange(
ctx,
r.URL.Query().Get("code"),
oauth2.SetAuthURLParam(pkce.ParamCodeVerifier, a.verifier),
)
rawIDToken, _ := token.Extra("id_token").(string)
idToken, _ := a.provider.Verifier(&oidc.Config{ClientID: a.auth.ClientID}).Verify(ctx, rawIDToken)
_ = idToken.Claims(&a.claims)
// Wails needs a runtime.WindowOpenURL(ctx, "url") equivalent of runtime.BrowserOpenURL(ctx, "url") perhaps? If the
// window has navigated away from wails://wails then there is no easy way to navigate back unless the page the window
// is displaying happens to be under your control (like this handler). If this is the case you can't 301 to
// wails://wails but you can in javascript. No idea why.
w.Header().Add("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "<script>window.location.href=%q</script>", FrontendURI)
})
go http.ListenAndServe(":9991", nil)
}
// domReady is called after the front-end dom has been loaded
func (a App) domReady(ctx context.Context) {
// Add your action here
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s!", name)
}
func (a *App) AuthURL() string {
challenge := pkce.CodeChallengeS256(a.verifier)
url := a.auth.AuthCodeURL(
"state",
oauth2.SetAuthURLParam(pkce.ParamCodeChallenge, challenge),
oauth2.SetAuthURLParam(pkce.ParamCodeChallengeMethod, pkce.MethodS256),
)
fmt.Printf("auth url: %s\n", url)
return url
}
func (a *App) AuthEmail() string {
if a.claims != nil {
return a.claims.Email
}
return ""
}
<script>
const login = async () => {
window.location.href = await window.go.main.App.AuthURL();
};
let email;
window.go.main.App.AuthEmail()
.then(e => {
if (!e) login();
email = e;
});
</script>
<main>
<h1>Email: {email}</h1>
</main>
<style>
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
main {
text-align: center;
padding: 1em;
margin: 0 auto;
}
img {
height: 16rem;
width: 16rem;
}
h1 {
@apply text-3xl font-bold underline text-emerald-700;
/*color: #ff3e00;
text-transform: uppercase;
font-size: 4rem;
font-weight: 100;*/
line-height: 1.1;
margin: 2rem auto;
max-width: 14rem;
}
p {
max-width: 14rem;
margin: 1rem auto;
line-height: 1.35;
}
@media (min-width: 480px) {
h1 {
max-width: none;
}
p {
max-width: none;
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment