Skip to content

Instantly share code, notes, and snippets.

@dabio
Created January 29, 2019 20:11
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 dabio/535fa216d9b7329eab9fe334c3f5abfb to your computer and use it in GitHub Desktop.
Save dabio/535fa216d9b7329eab9fe334c3f5abfb to your computer and use it in GitHub Desktop.
package fireauth
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const (
libraryVersion = "0.1.0"
defaultBaseURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/"
defaultTimeout = 5
defaultUserAgent = "fireauth/" + libraryVersion
defaultMediaType = "application/json"
signUpPath = "signupNewUser"
signInPath = "verifyPassword"
deleteAccountPath = "deleteAccount"
setAccountInfoPath = "setAccountInfo"
)
// Config for client.
type Config struct {
HTTPClient *http.Client
BaseURL string
APIKey string
UserAgent string
}
// NewConfig returns a new connfiguration required for the Client.
func NewConfig(apiKey string) *Config {
return &Config{
HTTPClient: &http.Client{Timeout: defaultTimeout * time.Second},
BaseURL: defaultBaseURL,
APIKey: apiKey,
UserAgent: defaultUserAgent,
}
}
// User contains all information you'll retrieve from the firebase API which
// is necessary to work with for further requests.
type User struct {
ID string `json:"localId"`
Email string `json:"email"`
RefreshToken string `json:"refreshToken"`
IDToken string `json:"idToken"`
}
// Client manages communication with the Firebase Authentication API.
type Client struct {
*Config
}
// NewClient returns an API client to work with Firebase Authentication.
func NewClient(config *Config) *Client {
return &Client{Config: config}
}
// NewRequest creates an API request. A relative URL can be provides in
// urlStr, which will be resolved to BaseURL of the Client. Relative URLs
// should always be specified without the preceding slash. If specified, the
// value pointed to by body is JSON encoded and included as the request body.
func (c *Client) NewRequest(urlStr string, body interface{}) (*http.Request, error) {
url := fmt.Sprintf("%s%s?key=%s", c.BaseURL, urlStr, c.APIKey)
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(body); err != nil {
return nil, err
}
}
req, err := http.NewRequest(http.MethodPost, url, buf)
if err != nil {
return nil, err
}
req.Header.Add("User-Agent", c.UserAgent)
req.Header.Add("Content-Type", defaultMediaType)
req.Header.Add("Accept", defaultMediaType)
return req, nil
}
// Do sends an API request and returns the API response. The API response is
// JSON decoded and stored in the value pointed to by v, or returned as an
// error if an API error occured. If v implements the io.Writer interface,
// the raw response will be written to v, without attempting to decode it.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
req = req.WithContext(ctx)
resp, err := c.HTTPClient.Do(req)
if err != nil {
// If we got an error and the context has been canceled, the context's
// error is probably more useful.
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return resp, fmt.Errorf("API status code was %d", resp.StatusCode)
}
if v != nil {
if w, ok := v.(io.Writer); ok {
if _, err = io.Copy(w, resp.Body); err != nil {
return resp, err
}
} else {
if err = json.NewDecoder(resp.Body).Decode(v); err != nil {
return resp, err
}
}
}
return resp, err
}
// Wrapper function for all API requests that should return a user object.
func (c *Client) returnUserRequest(ctx context.Context, basePath string, data interface{}) (*User, *http.Response, error) {
req, err := c.NewRequest(basePath, data)
if err != nil {
return nil, nil, err
}
r := new(User)
resp, err := c.Do(ctx, req, r)
return r, resp, err
}
type signUpRequest struct {
Email string `json:"email"`
Password string `json:"password"`
SecureToken bool `json:"returnSecureToken"`
}
// SignUp creates a new email and password user.
func (c *Client) SignUp(ctx context.Context, email, password string) (*User, *http.Response, error) {
data := &signUpRequest{
Email: email,
Password: password,
SecureToken: true,
}
return c.returnUserRequest(ctx, signUpPath, data)
}
type signInRequest struct {
Email string `json:"email"`
Password string `json:"password"`
SecureToken bool `json:"returnSecureToken"`
}
// SignIn signs in a user with email and password.
func (c *Client) SignIn(ctx context.Context, email, password string) (*User, *http.Response, error) {
data := &signInRequest{
Email: email,
Password: password,
SecureToken: true,
}
return c.returnUserRequest(ctx, signInPath, data)
}
// VerifyPassword is an alias for SignIn().
func (c *Client) VerifyPassword(ctx context.Context, email, password string) (*User, *http.Response, error) {
return c.SignIn(ctx, email, password)
}
type changeEmailRequest struct {
IDToken string `json:"idToken"`
Email string `json:"email"`
SecureToken bool `json:"returnSecureToken"`
}
// ChangeEmail changes a user's email.
func (c *Client) ChangeEmail(ctx context.Context, idToken, email string) (*User, *http.Response, error) {
data := &changeEmailRequest{
IDToken: idToken,
Email: email,
SecureToken: true,
}
return c.returnUserRequest(ctx, setAccountInfoPath, data)
}
type changePasswordRequest struct {
IDToken string `json:"idToken"`
Password string `json:"password"`
SecureToken bool `json:"returnSecureToken"`
}
// ChangePassword changes a user's password.
func (c *Client) ChangePassword(ctx context.Context, idToken, password string) (*User, *http.Response, error) {
data := &changePasswordRequest{
IDToken: idToken,
Password: password,
SecureToken: true,
}
return c.returnUserRequest(ctx, setAccountInfoPath, data)
}
type deleteAccountRequest struct {
IDToken string `json:"idToken"`
}
// DeleteAccount removes the given user.
func (c *Client) DeleteAccount(ctx context.Context, idToken string) (*http.Response, error) {
data := &deleteAccountRequest{IDToken: idToken}
req, err := c.NewRequest(deleteAccountPath, data)
if err != nil {
return nil, err
}
resp, err := c.Do(ctx, req, nil)
return resp, err
}
package fireauth_test
import (
"context"
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/dabio/fireauth"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyz"
var (
mux *http.ServeMux
server *httptest.Server
config *fireauth.Config
client *fireauth.Client
)
func init() {
rand.Seed(time.Now().UnixNano())
}
func setup() {
mux = http.NewServeMux()
server = httptest.NewServer(mux)
config = fireauth.NewConfig("")
config.BaseURL = server.URL
client = fireauth.NewClient(config)
}
func teardown() {
server.Close()
}
func randString(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
func randEmail() string {
return fmt.Sprintf("%s@%s.%s", randString(10), randString(10), randString(3))
}
func createUser(c *fireauth.Client) (user *fireauth.User, email string, password string, err error) {
email = randEmail()
password = randString(12)
user, _, err = c.SignUp(context.Background(), email, password)
return
}
func TestNewConfig(t *testing.T) {
t.Parallel()
s := randString(10)
c := fireauth.NewConfig(s)
if !strings.HasPrefix(c.UserAgent, "fireauth/") {
t.Error("UserAgent is not set correctly")
}
if !strings.HasPrefix(c.BaseURL, "https://") {
t.Error("BaseURL is wrong")
}
if c.APIKey != s {
t.Error("APIKey is wrong")
}
}
func TestNewRequest(t *testing.T) {
t.Parallel()
inBody, outBody := struct {
Foo string `json:"foo"`
}{"bar"}, `{"foo":"bar"}`+"\n"
s := randString(10)
c := fireauth.NewClient(fireauth.NewConfig(s))
req, _ := c.NewRequest("inURL", inBody)
if !strings.HasPrefix(req.Header.Get("User-Agent"), "fireauth/") {
t.Errorf("NewRequest User-Agent is wrong")
}
// test for json encoded body
if got, _ := ioutil.ReadAll(req.Body); string(got) != outBody {
t.Errorf("NewRequest Body is %v, want %v", string(got), outBody)
}
}
func TestDo(t *testing.T) {
setup()
defer teardown()
type foo struct {
A string
}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if got, want := r.Method, http.MethodPost; got != want {
t.Errorf("Request method = %v, want %v", got, want)
}
fmt.Fprint(w, `{"A":"a"}`)
})
req, _ := client.NewRequest("/", nil)
body := new(foo)
if _, err := client.Do(context.Background(), req, body); err != nil {
t.Fatalf("Do(): %v", err)
}
want := &foo{"a"}
if !reflect.DeepEqual(body, want) {
t.Errorf("Response body = %v, want %v", body, want)
}
}
func TestDo_httpError(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request", 400)
})
req, _ := client.NewRequest("/", nil)
if _, err := client.Do(context.Background(), req, nil); err == nil {
t.Error("Want HTTP 400 error.")
}
}
func TestSignUpIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
c := fireauth.NewClient(fireauth.NewConfig(os.Getenv("FIREBASE_API_KEY")))
u, email, _, err := createUser(c)
if err != nil {
t.Errorf("SignUp returned error: %v", err)
}
if email != u.Email {
t.Errorf("SignUp wrong email: %v, wants %v", u.Email, email)
}
c.DeleteAccount(context.Background(), u.IDToken)
}
func TestSignInIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
c := fireauth.NewClient(fireauth.NewConfig(os.Getenv("FIREBASE_API_KEY")))
u, _, password, _ := createUser(c)
u, _, err := c.SignIn(context.Background(), u.Email, password)
if err != nil {
t.Errorf("SignIn returned error: %v", err)
}
c.DeleteAccount(context.Background(), u.IDToken)
}
func TestChangeEmailIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
c := fireauth.NewClient(fireauth.NewConfig(os.Getenv("FIREBASE_API_KEY")))
u1, _, _, _ := createUser(c)
u2, _, _ := c.ChangeEmail(context.Background(), u1.IDToken, randEmail())
if u1.Email == u2.Email {
t.Errorf("ChangeEmail: email %v didn't change to %v", u1.Email, u2.Email)
}
c.DeleteAccount(context.Background(), u1.IDToken)
}
func TestChangePasswordIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
c := fireauth.NewClient(fireauth.NewConfig(os.Getenv("FIREBASE_API_KEY")))
u, _, _, _ := createUser(c)
_, _, err := c.ChangePassword(context.Background(), u.IDToken, randString(10))
if err != nil {
t.Errorf("ChangePassword failed with error: %v", err)
}
c.DeleteAccount(context.Background(), u.IDToken)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment