Last active
September 26, 2018 14:41
-
-
Save finnsson/9ede6db4644bcf78972f58df1b119908 to your computer and use it in GitHub Desktop.
GII - Swedish BankID - headless Go example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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