Created
January 29, 2019 20:11
-
-
Save dabio/535fa216d9b7329eab9fe334c3f5abfb to your computer and use it in GitHub Desktop.
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 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 | |
} |
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 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