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