Skip to content

Instantly share code, notes, and snippets.

@finnsson
Last active September 26, 2018 14:41
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 finnsson/9ede6db4644bcf78972f58df1b119908 to your computer and use it in GitHub Desktop.
Save finnsson/9ede6db4644bcf78972f58df1b119908 to your computer and use it in GitHub Desktop.
GII - Swedish BankID - headless Go example
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/go-chi/chi"
"github.com/google/uuid"
cache "github.com/patrickmn/go-cache"
"github.com/pkg/errors"
)
// CreateResponse is the response from a POST (auth or sign)
type CreateResponse struct {
AutoStartToken string
AutoStartURL string
QR string
Location string
}
// User info
type User struct {
PersonalNumber string
Name string
GivenName string
Surname string
}
// CompletionData from a complete auth/sign
type CompletionData struct {
User *User
}
// PollResponse is the reponse from a GET (auth or sign)
type PollResponse struct {
MessageSV string
MessageEN string
CompletionData *CompletionData
}
func main() {
router := chi.NewRouter()
// "https://demo-api.gii.cloud" for test and "https://api.gii.cloud" for prod
apiHost := os.Getenv("GII_HOST")
// set a public IP of user during test - otherwise local IP is used
publicIP := os.Getenv("PUBLIC_IP")
// your clientID
clientID := os.Getenv("CLIENT_ID")
// your clientSecret
clientSecret := os.Getenv("CLIENT_SECRET")
// you'll probably store the sessions in a DB or
sessionCache := cache.New(time.Hour*1, time.Hour*1)
router.Get("/auth", pollResponse(sessionCache, clientID, clientSecret))
router.Post("/auth", postAuth(sessionCache, clientID, clientSecret, publicIP, apiHost))
router.Get("/sign", pollResponse(sessionCache, clientID, clientSecret))
router.Post("/sign", postSign(sessionCache, clientID, clientSecret, publicIP, apiHost))
router.Get("/", index())
log.Printf("main: server listening on :8083")
http.ListenAndServe(":8083", router)
}
func writeHTMLHeaders(rw http.ResponseWriter) {
rw.Header().Set("Cache-Control", "private, no-cache, no-store, must-revalidate, max-age=0, s-maxage=0")
rw.Header().Set("Pragma", "no-cache")
rw.Header().Set("Expires", "0")
rw.Header().Set("Content-Type", "text/html; charset=utf-8")
}
func writeErr(rw http.ResponseWriter, err error) {
log.Printf("%+v", err)
writeHTMLHeaders(rw)
rw.WriteHeader(http.StatusBadRequest)
}
func index() http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
rw.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>BankID</title></head>
<body>
<form action="/auth" method="post">
<button type="submit">Authenticate</button>
</form>
<form action="/sign" method="post">
<label>Visible text:
<input type="text" name="visible_text" />
</label>
<label>Hidden text:
<input type="text" name="hidden_text" />
</label>
<button type="submit">Sign</button>
</form>
</body>
</html>
`))
}
}
func postAuth(sessionCache *cache.Cache, clientID string, clientSecret string, publicIP string, apiHost string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
client := http.Client{}
ip := publicIP
if ip == "" {
ip = req.RemoteAddr
}
data := url.Values{
"ip": []string{ip},
"get_qr": []string{"true"},
"autostart_token_required": []string{"true"},
}
authReq, err := http.NewRequest("POST", apiHost+"/api/ip/bankid-se/s2s/auth", strings.NewReader(data.Encode()))
if err != nil {
writeErr(rw, errors.Wrap(err, "invalid request"))
return
}
authReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
authReq.SetBasicAuth(clientID, clientSecret)
res, err := client.Do(authReq)
if err != nil {
writeErr(rw, errors.Wrap(err, "request failed"))
return
}
defer res.Body.Close()
if res.StatusCode >= 400 {
writeErr(rw, errors.Errorf("invalid response with http status code (%d)", res.StatusCode))
return
}
authResponse := CreateResponse{}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
writeErr(rw, errors.Wrap(err, "failed reading response body"))
return
}
err = json.Unmarshal(body, &authResponse)
if err != nil {
writeErr(rw, errors.Wrapf(err, "unmarhal failed for (%s)", body))
return
}
authResponse.Location = res.Header.Get("location")
sessionID := uuid.New().String()
sessionCache.SetDefault(sessionID, &authResponse)
writeHTMLHeaders(rw)
rw.Write([]byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<!-- display qr code to scan. Use the QR-response -->
<img src="%s" width="200" height="200" />
<script>
// try to start BankID automatically. Use the AutoStartURL-response.
location.href = "%s";
// automatically reload page after 2 sec
setTimeout(function() {
location.href = "http://localhost:8083/auth?id=%s";
}, 2000);
</script>
</body>
`, authResponse.QR, authResponse.AutoStartURL, sessionID)))
}
}
func postSign(sessionCache *cache.Cache, clientID string, clientSecret string, publicIP string, apiHost string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
client := http.Client{}
ip := publicIP
if ip == "" {
ip = req.RemoteAddr
}
data := url.Values{
"ip": []string{ip},
"get_qr": []string{"true"},
"autostart_token_required": []string{"true"},
"visible_text": []string{req.FormValue("visible_text")},
"hidden_text": []string{req.FormValue("hidden_text")},
}
authReq, err := http.NewRequest("POST", apiHost+"/api/ip/bankid-se/s2s/sign", strings.NewReader(data.Encode()))
if err != nil {
writeErr(rw, errors.Wrap(err, "invalid request"))
return
}
authReq.Header.Add("Content-Type", "application/x-www-form-urlencoded")
authReq.SetBasicAuth(clientID, clientSecret)
res, err := client.Do(authReq)
if err != nil {
writeErr(rw, errors.Wrap(err, "request failed"))
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
writeErr(rw, errors.Wrap(err, "failed reading response body"))
return
}
if res.StatusCode >= 400 {
writeErr(rw, errors.Errorf("invalid response with http status code (%d) and body (%s)", res.StatusCode, body))
return
}
authResponse := CreateResponse{}
err = json.Unmarshal(body, &authResponse)
if err != nil {
writeErr(rw, errors.Wrapf(err, "unmarhal failed for (%s)", body))
return
}
authResponse.Location = res.Header.Get("location")
sessionID := uuid.New().String()
sessionCache.SetDefault(sessionID, &authResponse)
writeHTMLHeaders(rw)
rw.Write([]byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<!-- display qr code to scan. Use the QR-response -->
<img src="%s" width="200" height="200" />
<script>
// try to start BankID automatically. Use the AutoStartURL-response.
location.href = "%s";
// automatically reload page after 2 sec
setTimeout(function() {
location.href = "http://localhost:8083/sign?id=%s";
}, 2000);
</script>
</body>
`, authResponse.QR, authResponse.AutoStartURL, sessionID)))
}
}
func pollResponse(sessionCache *cache.Cache, clientID string, clientSecret string) http.HandlerFunc {
return func(rw http.ResponseWriter, req *http.Request) {
// get sessionID
sessionID := req.FormValue("id")
authResponseInterface, ok := sessionCache.Get(sessionID)
if !ok {
writeErr(rw, errors.New("invalid id"))
return
}
authResponse, ok := authResponseInterface.(*CreateResponse)
if !ok {
writeErr(rw, errors.New("invalid type of authResponse"))
return
}
// check Location
client := http.Client{}
authReq, err := http.NewRequest("GET", authResponse.Location, nil)
if err != nil {
writeErr(rw, errors.Wrap(err, "invalid request"))
return
}
authReq.SetBasicAuth(clientID, clientSecret)
res, err := client.Do(authReq)
if err != nil {
writeErr(rw, errors.Wrap(err, "request failed"))
return
}
defer res.Body.Close()
pollResponse := PollResponse{}
body, err := ioutil.ReadAll(res.Body)
err = json.Unmarshal(body, &pollResponse)
if err != nil {
writeErr(rw, errors.Wrap(err, "unmarshal failed"))
return
}
switch res.StatusCode {
case 200:
// ok - show result
user := pollResponse.CompletionData.User
writeHTMLHeaders(rw)
rw.Write([]byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<!-- display message in Swedish. Use the MessageSV-response -->
<p>Given name: %s</p>
<p>Surname: %s</p>
<p>Name: %s</p>
<p>Personal number: %s</p>
</body>
`, user.GivenName, user.Surname, user.Name, user.PersonalNumber)))
break
case 202:
// continue to poll - show message
writeHTMLHeaders(rw)
rw.Write([]byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<!-- display qr code to scan. Use the QR-response -->
<img src="%s" width="200" height="200" />
<!-- display message in Swedish. Use the MessageSV-response -->
<p>%s</p>
<script>
// automatically reload page after 2 sec
setTimeout(function() {
location.reload();
}, 2000);
</script>
</body>
`, authResponse.QR, pollResponse.MessageSV)))
break
default:
// some kind of error - show message
writeHTMLHeaders(rw)
rw.Write([]byte(fmt.Sprintf(`
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
</head>
<body>
<!-- display message in Swedish. Use the MessageSV-response -->
<p>Error: %s</p>
</body>
`, pollResponse.MessageSV)))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment