package main import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/ecdh" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "github.com/cespare/xxhash/v2" "net/http" "net/url" "strconv" "strings" "syscall/js" ) func sha256Base64(s string) string { hashed := sha256.Sum256([]byte(s)) encoded := base64.URLEncoding.EncodeToString(hashed[:]) encoded = strings.TrimRight(encoded, "=") return encoded } func randomChars(length int) (string, error) { var saltChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" if length <= 0 { return "", errors.New("salt length must be greater than 0") } salt := make([]byte, length) randomBytes := make([]byte, length) _, err := rand.Read(randomBytes) if err != nil { return "", err } for i := range salt { salt[i] = saltChars[int(randomBytes[i])%len(saltChars)] } return string(salt), nil } func main() { // Redirect to log-in if not signed in localStorage := js.Global().Get("localStorage") if localStorage.Call("getItem", "DONOTSHARE-secretKey").IsNull() { js.Global().Get("window").Get("location").Call("replace", "/login"+js.Global().Get("window").Get("location").Get("search").String()) } statusBox := js.Global().Get("document").Call("getElementById", "statusBox") authorizeButton := js.Global().Get("document").Call("getElementById", "authorizeButton") // Check if the token is valid requestUri, err := url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/loggedIn") if err != nil { statusBox.Set("innerText", "Error joining URL: "+err.Error()) return } loggedInBody := map[string]interface{}{ "token": localStorage.Call("getItem", "DONOTSHARE-secretKey").String(), } // Marshal the body body, err := json.Marshal(loggedInBody) if err != nil { statusBox.Set("innerText", "Error marshaling signup body: "+err.Error()) return } response, err := http.Post(requestUri, "application/json", bytes.NewReader(body)) if err != nil { statusBox.Set("innerText", "Error contacting server: "+err.Error()) return } // Check if the response is 200 if response.StatusCode == 401 { // Close the response body err = response.Body.Close() if err != nil { fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") } // Redirect to log-out if not signed in js.Global().Get("window").Get("location").Call("replace", "/logout"+js.Global().Get("window").Get("location").Get("search").String()) return } else if response.StatusCode == 500 { // Read the response var responseMap map[string]interface{} decoder := json.NewDecoder(response.Body) err = decoder.Decode(&responseMap) if err != nil { js.Global().Call("alert", "Error decoding server response: "+err.Error()) return } // Close the response body err = response.Body.Close() if err != nil { fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") } // Alert the user if the server is down js.Global().Call("alert", "Something went wrong! (error code: "+responseMap["code"].(string)+")") return } // Close the response body err = response.Body.Close() if err != nil { fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") } // Check if the URL has a code urlParams, err := url.ParseQuery(strings.TrimPrefix(js.Global().Get("window").Get("location").Get("search").String(), "?")) if err != nil { statusBox.Set("innerText", "Error parsing URL: "+err.Error()) return } if urlParams.Has("code") { // Set the status box statusBox.Set("innerText", "Authenticating...") // Create the form data var formData = url.Values{} formData.Set("grant_type", "authorization_code") formData.Set("code", urlParams.Get("code")) formData.Set("client_id", "TestApp-DoNotUse") formData.Set("redirect_uri", js.Global().Get("window").Get("location").Get("origin").String()+"/testApp") formData.Set("code_verifier", localStorage.Call("getItem", "TESTER-verifier").String()) // Create the request requestUri, err := url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/oauth/token") if err != nil { statusBox.Set("innerText", "Error joining URL: "+err.Error()) return } response, err := http.Post(requestUri, "application/x-www-form-urlencoded", strings.NewReader(formData.Encode())) if err != nil { statusBox.Set("innerText", "Error contacting server: "+err.Error()) return } // Read the response var responseMap map[string]interface{} decoder := json.NewDecoder(response.Body) err = decoder.Decode(&responseMap) if err != nil { statusBox.Set("innerText", "Error decoding server response: "+err.Error()) return } // Close the response body err = response.Body.Close() if err != nil { fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") } if response.StatusCode == 200 { // Fetch userinfo requestUri, err := url.JoinPath(js.Global().Get("window").Get("location").Get("origin").String(), "/api/oauth/userinfo") if err != nil { statusBox.Set("innerText", "Error joining URL: "+err.Error()) return } // Create the request request, err := http.NewRequest("GET", requestUri, nil) if err != nil { statusBox.Set("innerText", "Error creating request: "+err.Error()) return } // Set the authorization header request.Header.Set("Authorization", "Bearer "+responseMap["id_token"].(string)) // Send the request response, err := http.DefaultClient.Do(request) if err != nil { statusBox.Set("innerText", "Error contacting server: "+err.Error()) return } // Read the response decoder = json.NewDecoder(response.Body) err = decoder.Decode(&responseMap) if err != nil { statusBox.Set("innerText", "Error decoding server response: "+err.Error()) return } // Close the response body err = response.Body.Close() if err != nil { fmt.Println("Could not close response body: " + err.Error() + ", memory leaks may occur") } // Set the username localStorage.Call("setItem", "TESTER-username", responseMap["username"].(string)) // Generate the keypair privateKey, err := ecdh.X25519().GenerateKey(rand.Reader) if err != nil { statusBox.Set("innerText", "Error generating private key: "+err.Error()) return } // Save the private key localStorage.Call("setItem", "TESTER-privateKey", base64.StdEncoding.EncodeToString(privateKey.Bytes())) // Redirect to the client key exchange endpoint js.Global().Get("window").Get("location").Call("replace", "/clientKeyShare?ecdhPublicKey="+base64.URLEncoding.EncodeToString(privateKey.PublicKey().Bytes())+"&accessToken="+responseMap["access_token"].(string)) return } else if response.StatusCode != 500 { statusBox.Set("innerText", responseMap["error"].(string)) } else { statusBox.Set("innerText", "Something went wrong! (error code: "+responseMap["code"].(string)+")") } } else if urlParams.Has("error") { if urlParams.Get("error") == "access_denied" { statusBox.Set("innerText", "Access denied") } else { statusBox.Set("innerText", "Authentication failed (error code: "+urlParams.Get("error")+")") } } else if urlParams.Has("ecdhPublicKey") { publicKeyByte, err := base64.URLEncoding.DecodeString(urlParams.Get("ecdhPublicKey")) if err != nil { statusBox.Set("innerText", "Error decoding public key: "+err.Error()) return } publicKey, err := ecdh.X25519().NewPublicKey(publicKeyByte) if err != nil { statusBox.Set("innerText", "Error encoding public key: "+err.Error()) return } privateKeyBytes, err := base64.StdEncoding.DecodeString(localStorage.Call("getItem", "TESTER-privateKey").String()) if err != nil { statusBox.Set("innerText", "Error decoding private key: "+err.Error()) return } privateKey, err := ecdh.X25519().NewPrivateKey(privateKeyBytes) if err != nil { statusBox.Set("innerText", "Error encoding private key: "+err.Error()) return } nonce, err := base64.URLEncoding.DecodeString(urlParams.Get("nonce")) if err != nil { statusBox.Set("innerText", "Error decoding nonce: "+err.Error()) return } ciphertext, err := base64.URLEncoding.DecodeString(urlParams.Get("cipherText")) if err != nil { statusBox.Set("innerText", "Error decoding ciphertext: "+err.Error()) return } // Generate the shared secret sharedSecret, err := privateKey.ECDH(publicKey) if err != nil { statusBox.Set("innerText", "Error generating shared secret: "+err.Error()) return } // Decrypt the ciphertext block, err := aes.NewCipher(sharedSecret) if err != nil { statusBox.Set("innerText", "Error creating AES cipher: "+err.Error()) return } gcm, err := cipher.NewGCM(block) if err != nil { statusBox.Set("innerText", "Error creating GCM cipher: "+err.Error()) return } plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) if err != nil { statusBox.Set("innerText", "Error decrypting ciphertext: "+err.Error()) return } statusBox.Set("innerText", "Authentication complete! Authenticated as "+localStorage.Call("getItem", "TESTER-username").String()+" with client key "+strconv.FormatUint(xxhash.Sum64(plaintext), 10)) // Remove all our temporary data localStorage.Call("removeItem", "TESTER-verifier") localStorage.Call("removeItem", "TESTER-username") localStorage.Call("removeItem", "TESTER-privateKey") } // Add event listener for authorize button authorizeButton.Call("addEventListener", "click", js.FuncOf(func(this js.Value, p []js.Value) interface{} { go func() { verifier, err := randomChars(128) if err != nil { statusBox.Set("innerText", "Error generating verifier: "+err.Error()) return } // Generate the challenge verifierChallenge := sha256Base64(verifier) // Save the verifier localStorage.Call("setItem", "TESTER-verifier", verifier) // Redirect to the authorization page js.Global().Get("window").Get("location").Call("replace", "/authorize?response_type=code&client_id=TestApp-DoNotUse&redirect_uri="+url.QueryEscape(js.Global().Get("window").Get("location").Get("origin").String()+"/testApp")+"&code_challenge="+verifierChallenge+"&code_challenge_method=S256") }() return nil })) // Wait for events select {} }