drasl/auth_test.go

601 lines
19 KiB
Go

package main
import (
"encoding/json"
"github.com/samber/mo"
"github.com/stretchr/testify/assert"
"net/http"
"testing"
)
func TestAuth(t *testing.T) {
t.Parallel()
{
ts := &TestSuite{}
config := testConfig()
ts.Setup(config)
defer ts.Teardown()
ts.CreateTestUser(t, ts.App, ts.Server, TEST_USERNAME)
ts.CreateTestUser(t, ts.App, ts.Server, TEST_OTHER_USERNAME)
t.Run("Test /", ts.testGetServerInfo)
t.Run("Test /authenticate", ts.testAuthenticate)
t.Run("Test /authenticate, multiple profiles", ts.testAuthenticateMultipleProfiles)
t.Run("Test /invalidate", ts.testInvalidate)
t.Run("Test /refresh", ts.testRefresh)
t.Run("Test /signout", ts.testSignout)
t.Run("Test /validate", ts.testValidate)
t.Run("Test authenticate with duplicate client token", ts.testDuplicateClientToken)
}
}
func (ts *TestSuite) testGetServerInfo(t *testing.T) {
rec := ts.Get(t, ts.Server, "/auth", nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
}
func (ts *TestSuite) authenticate(t *testing.T, username string, password string) *authenticateResponse {
authenticatePayload := authenticateRequest{
Username: username,
Password: password,
RequestUser: false,
}
rec := ts.PostJSON(t, ts.Server, "/authenticate", authenticatePayload, nil, nil)
// Authentication should succeed and we should get a valid clientToken and
// accessToken
assert.Equal(t, http.StatusOK, rec.Code)
var authenticateRes authenticateResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&authenticateRes))
assert.Equal(t, 32, len(authenticateRes.ClientToken))
clientToken := authenticateRes.ClientToken
accessToken := authenticateRes.AccessToken
// Check that the access token is valid
client := ts.App.GetClient(accessToken, StalePolicyDeny)
assert.NotNil(t, client)
assert.Equal(t, client.ClientToken, clientToken)
return &authenticateRes
}
func (ts *TestSuite) testAuthenticate(t *testing.T) {
{
// Successful authentication
response := ts.authenticate(t, TEST_PLAYER_NAME, TEST_PASSWORD)
// We did not pass an agent
assert.Nil(t, response.SelectedProfile)
assert.Nil(t, response.AvailableProfiles)
// We did not pass requestUser
assert.Nil(t, response.User)
}
{
// If we send our own clientToken, the server should use it
clientToken := "12345678901234567890123456789012"
payload := authenticateRequest{
Username: TEST_PLAYER_NAME,
Password: TEST_PASSWORD,
ClientToken: &clientToken,
RequestUser: false,
}
rec := ts.PostJSON(t, ts.Server, "/authenticate", payload, nil, nil)
// Authentication should succeed and we should get a valid clientToken and
// accessToken
assert.Equal(t, http.StatusOK, rec.Code)
var response authenticateResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, clientToken, response.ClientToken)
// Check that the database was updated
var client Client
result := ts.App.DB.Preload("Player").First(&client, "client_token = ?", response.ClientToken)
assert.Nil(t, result.Error)
assert.NotNil(t, client.Player)
assert.Equal(t, TEST_PLAYER_NAME, client.Player.Name)
accessTokenClient := ts.App.GetClient(response.AccessToken, StalePolicyDeny)
assert.NotNil(t, accessTokenClient)
accessTokenClient.Player = client.Player
accessTokenClient.User = client.User
assert.Equal(t, client, *accessTokenClient)
// The accessToken should be valid
validatePayload := validateRequest{
ClientToken: response.ClientToken,
AccessToken: response.AccessToken,
}
rec = ts.PostJSON(t, ts.Server, "/validate", validatePayload, nil, nil)
assert.Equal(t, http.StatusNoContent, rec.Code)
// Authentication should succeed if we POST /authenticate again with
// the same clientToken
payload = authenticateRequest{
Username: TEST_PLAYER_NAME,
Password: TEST_PASSWORD,
ClientToken: &clientToken,
RequestUser: false,
}
rec = ts.PostJSON(t, ts.Server, "/authenticate", payload, nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
var newResponse authenticateResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&newResponse))
assert.Equal(t, clientToken, newResponse.ClientToken)
result = ts.App.DB.First(&client, "client_token = ?", clientToken)
assert.Nil(t, result.Error)
// The old accessToken should be invalid
validatePayload = validateRequest{
ClientToken: response.ClientToken,
AccessToken: response.AccessToken,
}
rec = ts.PostJSON(t, ts.Server, "/validate", validatePayload, nil, nil)
assert.Equal(t, http.StatusForbidden, rec.Code)
// The new accessToken should be valid
validatePayload = validateRequest{
ClientToken: newResponse.ClientToken,
AccessToken: newResponse.AccessToken,
}
rec = ts.PostJSON(t, ts.Server, "/validate", validatePayload, nil, nil)
assert.Equal(t, http.StatusNoContent, rec.Code)
}
{
// Should fail when incorrect password is sent
payload := authenticateRequest{
Username: TEST_PLAYER_NAME,
Password: "incorrect",
ClientToken: nil,
RequestUser: false,
}
rec := ts.PostJSON(t, ts.Server, "/authenticate", payload, nil, nil)
// Authentication should fail
var response ErrorResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, "ForbiddenOperationException", *response.Error)
assert.Equal(t, "Invalid credentials. Invalid username or password.", *response.ErrorMessage)
}
{
// Should return a profile when the `agent` field is included in the request
payload := authenticateRequest{
Username: TEST_PLAYER_NAME,
Password: TEST_PASSWORD,
ClientToken: nil,
RequestUser: false,
Agent: &Agent{
Name: "Minecraft",
Version: 1,
},
}
rec := ts.PostJSON(t, ts.Server, "/authenticate", payload, nil, nil)
// Authentication should succeed
assert.Equal(t, http.StatusOK, rec.Code)
var response authenticateResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
var player Player
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_PLAYER_NAME).Error)
expectedProfile := Profile{
ID: Unwrap(UUIDToID(player.UUID)),
Name: player.Name,
}
assert.Equal(t, expectedProfile, *response.SelectedProfile)
assert.Equal(t, 1, len(*response.AvailableProfiles))
assert.Equal(t, expectedProfile, (*response.AvailableProfiles)[0])
}
{
// Should return a user when `requestUser` is true
payload := authenticateRequest{
Username: TEST_PLAYER_NAME,
Password: TEST_PASSWORD,
ClientToken: nil,
RequestUser: true,
}
rec := ts.PostJSON(t, ts.Server, "/authenticate", payload, nil, nil)
// Authentication should succeed
assert.Equal(t, http.StatusOK, rec.Code)
var response authenticateResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
var player Player
assert.Nil(t, ts.App.DB.Preload("User").First(&player, "name = ?", TEST_PLAYER_NAME).Error)
expectedUser := UserResponse{
ID: Unwrap(UUIDToID(player.User.UUID)),
Properties: []UserProperty{{
Name: "preferredLanguage",
Value: player.User.PreferredLanguage,
}},
}
assert.Equal(t, expectedUser, *response.User)
}
}
func (ts *TestSuite) testAuthenticateMultipleProfiles(t *testing.T) {
{
var user User
assert.Nil(t, ts.App.DB.First(&user, "username = ?", TEST_USERNAME).Error)
secondPlayerName := "SecondPlayer"
// player := user.Players[0]
otherPlayer, err := ts.App.CreatePlayer(&GOD, user.UUID, secondPlayerName, nil, false, nil, nil, nil, nil, nil, nil, nil)
assert.Nil(t, err)
authenticatePayload := authenticateRequest{
Username: TEST_USERNAME,
Password: TEST_PASSWORD,
RequestUser: false,
Agent: &Agent{
Name: "Minecraft",
Version: 1,
},
}
rec := ts.PostJSON(t, ts.Server, "/authenticate", authenticatePayload, nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
var authenticateRes authenticateResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&authenticateRes))
// We did not pass requestUser
assert.Nil(t, authenticateRes.User)
// User has multiple players, selectedProfile should be missing
assert.Nil(t, authenticateRes.SelectedProfile)
assert.Equal(t, 2, len(*authenticateRes.AvailableProfiles))
p := mo.None[Profile]()
for _, availableProfile := range *authenticateRes.AvailableProfiles {
if availableProfile.Name == secondPlayerName {
p = mo.Some(availableProfile)
break
}
}
profile, ok := p.Get()
assert.True(t, ok)
// Now, refresh to select a profile
refreshPayload := refreshRequest{
ClientToken: authenticateRes.ClientToken,
AccessToken: authenticateRes.AccessToken,
RequestUser: false,
SelectedProfile: &profile,
}
rec = ts.PostJSON(t, ts.Server, "/refresh", refreshPayload, nil, nil)
// Refresh should succeed and we should get a new accessToken
assert.Equal(t, http.StatusOK, rec.Code)
var refreshRes refreshResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&refreshRes))
assert.Equal(t, authenticateRes.ClientToken, refreshRes.ClientToken)
assert.NotEqual(t, authenticateRes.AccessToken, refreshRes.AccessToken)
assert.Equal(t, profile, *refreshRes.SelectedProfile)
assert.Nil(t, ts.App.DeletePlayer(&GOD, &otherPlayer))
}
}
func (ts *TestSuite) testInvalidate(t *testing.T) {
{
authenticateRes := ts.authenticate(t, TEST_PLAYER_NAME, TEST_PASSWORD)
clientToken := authenticateRes.ClientToken
accessToken := authenticateRes.AccessToken
// Successful invalidate
// We should start with valid clients in the database
client := ts.App.GetClient(accessToken, StalePolicyDeny)
assert.NotNil(t, client)
var clients []Client
result := ts.App.DB.Model(Client{}).Where("player_uuid = ?", &client.Player.UUID).Find(&clients)
assert.Nil(t, result.Error)
assert.True(t, len(clients) > 0)
oldVersions := make(map[string]int)
for _, client := range clients {
oldVersions[client.ClientToken] = client.Version
}
payload := invalidateRequest{
ClientToken: clientToken,
AccessToken: accessToken,
}
rec := ts.PostJSON(t, ts.Server, "/invalidate", payload, nil, nil)
// Invalidate should succeed
assert.Equal(t, http.StatusNoContent, rec.Code)
// The token version of each client should have been incremented,
// invalidating all previously-issued JWTs
assert.Nil(t, ts.App.GetClient(accessToken, StalePolicyDeny))
result = ts.App.DB.Model(Client{}).Where("player_uuid = ?", &client.Player.UUID).Find(&clients)
assert.Nil(t, result.Error)
for _, client := range clients {
assert.Equal(t, oldVersions[client.ClientToken]+1, client.Version)
}
}
{
// Re-authenticate
authenticateRes := ts.authenticate(t, TEST_PLAYER_NAME, TEST_PASSWORD)
clientToken := authenticateRes.ClientToken
// Invalidate should fail if we send an invalid access token
payload := refreshRequest{
ClientToken: clientToken,
AccessToken: "invalid",
}
rec := ts.PostJSON(t, ts.Server, "/invalidate", payload, nil, nil)
// Invalidate should fail
var response ErrorResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, http.StatusUnauthorized, rec.Code)
assert.Equal(t, "ForbiddenOperationException", *response.Error)
assert.Equal(t, "Invalid token.", *response.ErrorMessage)
}
}
func (ts *TestSuite) testRefresh(t *testing.T) {
// First, authenticate to get a token pair
authenticateRes := ts.authenticate(t, TEST_PLAYER_NAME, TEST_PASSWORD)
clientToken := authenticateRes.ClientToken
accessToken := authenticateRes.AccessToken
{
// Successful refresh
payload := refreshRequest{
ClientToken: clientToken,
AccessToken: accessToken,
RequestUser: false,
}
rec := ts.PostJSON(t, ts.Server, "/refresh", payload, nil, nil)
// Refresh should succeed and we should get a new accessToken
assert.Equal(t, http.StatusOK, rec.Code)
var refreshRes refreshResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&refreshRes))
assert.Equal(t, clientToken, refreshRes.ClientToken)
assert.NotEqual(t, accessToken, refreshRes.AccessToken)
// The old accessToken should be invalid
client := ts.App.GetClient(accessToken, StalePolicyDeny)
assert.Nil(t, client)
// The new token should be valid
client = ts.App.GetClient(refreshRes.AccessToken, StalePolicyDeny)
assert.NotNil(t, client)
// The response should include a profile
var player Player
assert.Nil(t, ts.App.DB.First(&player, "name = ?", TEST_PLAYER_NAME).Error)
expectedProfile := Profile{
ID: Unwrap(UUIDToID(player.UUID)),
Name: player.Name,
}
assert.Equal(t, expectedProfile, *refreshRes.SelectedProfile)
assert.Equal(t, []Profile{expectedProfile}, refreshRes.AvailableProfiles)
// We did not pass requestUser
assert.Nil(t, refreshRes.User)
// For future tests
accessToken = refreshRes.AccessToken
}
{
// Should return a user when `requestUser` is true
payload := refreshRequest{
ClientToken: clientToken,
AccessToken: accessToken,
RequestUser: true,
}
rec := ts.PostJSON(t, ts.Server, "/refresh", payload, nil, nil)
var refreshRes refreshResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&refreshRes))
var player Player
assert.Nil(t, ts.App.DB.Preload("User").First(&player, "name = ?", TEST_PLAYER_NAME).Error)
expectedUser := UserResponse{
ID: Unwrap(UUIDToID(player.UUID)),
Properties: []UserProperty{UserProperty{
Name: "preferredLanguage",
Value: player.User.PreferredLanguage,
}},
}
assert.Equal(t, expectedUser, *refreshRes.User)
accessToken = refreshRes.AccessToken
}
{
// Refresh should fail if we send an invalid client token
payload := refreshRequest{
ClientToken: "invalid",
AccessToken: accessToken,
RequestUser: false,
}
rec := ts.PostJSON(t, ts.Server, "/refresh", payload, nil, nil)
// Refresh should fail
var response ErrorResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, "ForbiddenOperationException", *response.Error)
}
{
// Refresh should fail if we send an invalid access token
payload := refreshRequest{
ClientToken: clientToken,
AccessToken: "invalid",
RequestUser: false,
}
rec := ts.PostJSON(t, ts.Server, "/refresh", payload, nil, nil)
// Refresh should fail
var response ErrorResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, "ForbiddenOperationException", *response.Error)
assert.Equal(t, "Invalid token.", *response.ErrorMessage)
}
}
func (ts *TestSuite) testSignout(t *testing.T) {
// First, authenticate so we have a valid client to test that it gets
// invalidated
authenticateRes := ts.authenticate(t, TEST_PLAYER_NAME, TEST_PASSWORD)
accessToken := authenticateRes.AccessToken
{
// Successful signout
var user User
result := ts.App.DB.First(&user, "username = ?", TEST_USERNAME)
assert.Nil(t, result.Error)
// We should start with valid clients in the database
client := ts.App.GetClient(accessToken, StalePolicyDeny)
assert.NotNil(t, client)
var clients []Client
result = ts.App.DB.Model(Client{}).Where("user_uuid = ?", client.UserUUID).Find(&clients)
assert.Nil(t, result.Error)
assert.True(t, len(clients) > 0)
oldVersions := make(map[string]int)
for _, client := range clients {
oldVersions[client.ClientToken] = client.Version
}
payload := signoutRequest{
Username: TEST_USERNAME,
Password: TEST_PASSWORD,
}
rec := ts.PostJSON(t, ts.Server, "/signout", payload, nil, nil)
// Signout should succeed
assert.Equal(t, http.StatusNoContent, rec.Code)
// The token version of each client should have been incremented,
// invalidating all previously-issued JWTs
assert.Nil(t, ts.App.GetClient(accessToken, StalePolicyDeny))
result = ts.App.DB.Model(Client{}).Where("user_uuid = ?", client.UserUUID).Find(&clients)
assert.Nil(t, result.Error)
assert.True(t, len(clients) > 0)
for _, client := range clients {
assert.Equal(t, oldVersions[client.ClientToken]+1, client.Version)
}
}
{
// Should fail when incorrect password is sent
payload := signoutRequest{
Username: TEST_USERNAME,
Password: "incorrect",
}
rec := ts.PostJSON(t, ts.Server, "/signout", payload, nil, nil)
// Signout should fail
var response ErrorResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, http.StatusUnauthorized, rec.Code)
assert.Equal(t, "ForbiddenOperationException", *response.Error)
assert.Equal(t, "Invalid credentials. Invalid username or password.", *response.ErrorMessage)
}
}
func (ts *TestSuite) testValidate(t *testing.T) {
// First, authenticate to get a token pair
authenticateRes := ts.authenticate(t, TEST_PLAYER_NAME, TEST_PASSWORD)
clientToken := authenticateRes.ClientToken
accessToken := authenticateRes.AccessToken
{
// Successful validate
payload := validateRequest{
ClientToken: clientToken,
AccessToken: accessToken,
}
rec := ts.PostJSON(t, ts.Server, "/validate", payload, nil, nil)
assert.Equal(t, http.StatusNoContent, rec.Code)
}
{
// Validate should fail if we send an invalid client token
payload := refreshRequest{
ClientToken: "invalid",
AccessToken: accessToken,
}
rec := ts.PostJSON(t, ts.Server, "/validate", payload, nil, nil)
assert.Equal(t, http.StatusForbidden, rec.Code)
}
{
// Validate should fail if we send an invalid client token
payload := refreshRequest{
ClientToken: clientToken,
AccessToken: "invalid",
}
rec := ts.PostJSON(t, ts.Server, "/validate", payload, nil, nil)
assert.Equal(t, http.StatusForbidden, rec.Code)
}
{
// Validate should fail if the token pair is invalid
var client Client
result := ts.App.DB.First(&client, "client_token = ?", clientToken)
assert.Nil(t, result.Error)
client.Version += 1
assert.Nil(t, ts.App.DB.Save(&client).Error)
payload := refreshRequest{
ClientToken: clientToken,
AccessToken: accessToken,
}
rec := ts.PostJSON(t, ts.Server, "/validate", payload, nil, nil)
assert.Equal(t, http.StatusForbidden, rec.Code)
}
}
func (ts *TestSuite) testDuplicateClientToken(t *testing.T) {
// Two users should be able to use the same clientToken
authenticateRes := ts.authenticate(t, TEST_PLAYER_NAME, TEST_PASSWORD)
clientToken := authenticateRes.ClientToken
payload := authenticateRequest{
Username: TEST_OTHER_USERNAME,
Password: TEST_PASSWORD,
ClientToken: &clientToken,
RequestUser: false,
}
rec := ts.PostJSON(t, ts.Server, "/authenticate", payload, nil, nil)
assert.Equal(t, http.StatusOK, rec.Code)
var response authenticateResponse
assert.Nil(t, json.NewDecoder(rec.Body).Decode(&response))
assert.Equal(t, clientToken, response.ClientToken)
var player Player
result := ts.App.DB.First(&player, "name = ?", TEST_PLAYER_NAME)
assert.Nil(t, result.Error)
var otherPlayer Player
result = ts.App.DB.First(&otherPlayer, "name = ?", TEST_OTHER_USERNAME)
assert.Nil(t, result.Error)
var client Client
result = ts.App.DB.Preload("Player").First(&client, "client_token = ? AND player_uuid = ?", clientToken, player.UUID)
assert.Nil(t, result.Error)
assert.Equal(t, TEST_PLAYER_NAME, client.Player.Name)
var otherClient Client
result = ts.App.DB.Preload("Player").First(&otherClient, "client_token = ? AND player_uuid = ?", clientToken, otherPlayer.UUID)
assert.Nil(t, result.Error)
assert.Equal(t, TEST_OTHER_USERNAME, otherClient.Player.Name)
}