Skip to content

Commit

Permalink
feat: interactive authentication through OAuth2 (#359)
Browse files Browse the repository at this point in the history
Co-authored-by: Arne Luenser <arne.luenser@ory.sh>
  • Loading branch information
zepatrik and alnr authored Jul 10, 2024
1 parent 164a94f commit cf39d4a
Show file tree
Hide file tree
Showing 60 changed files with 1,682 additions and 1,557 deletions.
4 changes: 2 additions & 2 deletions .docker/Dockerfile-alpine
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM alpine:3.19
FROM alpine:3.20

RUN addgroup -S ory; \
adduser -S ory -G ory -D -h /home/ory -s /bin/nologin; \
chown -R ory:ory /home/ory

RUN apk add -U --no-cache ca-certificates
RUN apk add -U --no-cache ca-certificates libssl3 libcrypto3

COPY ory /usr/bin/ory

Expand Down
5 changes: 3 additions & 2 deletions .docker/Dockerfile-build
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.22-alpine3.19 AS builder
FROM golang:1.22-alpine3.20 AS builder

RUN apk -U --no-cache add build-base git gcc bash

Expand All @@ -16,13 +16,14 @@ ADD . .

RUN CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -tags sqlite,json1 -o /usr/bin/ory

FROM alpine:3.19
FROM alpine:3.20

RUN addgroup -S ory; \
adduser -S ory -G ory -D -h /home/ory -s /bin/nologin; \
chown -R ory:ory /home/ory

RUN apk add -U --no-cache ca-certificates
RUN apk upgrade --no-cache libssl3 libcrypto3

COPY --from=builder /usr/bin/ory /usr/bin/ory

Expand Down
21 changes: 12 additions & 9 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,23 @@ jobs:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: abhi1693/setup-browser@v0.3.4
with:
browser: chrome
version: latest
- uses: ory/ci/checkout@master
- uses: actions/setup-go@v2
- uses: actions/setup-go@v5
with:
go-version: "1.22"
- run: |
make test
- uses: actions/cache@v4
with:
path: |
~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('go.sum') }}
restore-keys: ${{ runner.os }}-playwright-
- run: go run github.com/playwright-community/playwright-go/cmd/playwright install chromium --with-deps
- run: make lint
- run: go test ./...
env:
ORY_RATE_LIMIT_HEADER: ${{ secrets.ORY_RATE_LIMIT_HEADER }}
ORY_CLOUD_CONSOLE_URL: https://console.staging.ory.dev
ORY_CLOUD_ORYAPIS_URL: https://staging.oryapis.dev
ORY_CONSOLE_URL: https://console.staging.ory.dev
ORY_ORYAPIS_URL: https://staging.oryapis.dev

docs:
name: Generate docs
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ jobs:
ORY_API_KEY: nokey
ORY_PROJECT_SLUG: affectionate-archimedes-s9mkjq77k0
ORY_CONSOLE_URL: https://console.staging.ory.dev
ORY_ORYAPIS_URL: https://projects.staging.oryapis.dev
ORY_ORYAPIS_URL: https://staging.oryapis.dev
ORY_RATE_LIMIT_HEADER: ${{ secrets.ORY_RATE_LIMIT_HEADER }}
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export GO111MODULE := on
export PATH := .bin:${PATH}
export PWD := $(shell pwd)

GOLANGCI_LINT_VERSION = 1.55.2
GOLANGCI_LINT_VERSION = 1.59.1

GO_DEPENDENCIES = github.com/ory/go-acc \
github.com/golang/mock/mockgen \
Expand Down Expand Up @@ -49,10 +49,6 @@ lint: .bin/golangci-lint-$(GOLANGCI_LINT_VERSION)
install:
GO111MODULE=on go install -tags sqlite .

.PHONY: test
test: lint
go test -p 1 -tags sqlite -count=1 -failfast ./...

.PHONY: refresh
refresh:
UPDATE_SNAPSHOTS=true go test -tags sqlite,json1,refresh ./...
Expand Down
5 changes: 2 additions & 3 deletions cmd/cloudx/accountexperience/accountexperience.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"fmt"
"path"

"github.com/pkg/browser"
"github.com/pkg/errors"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -45,13 +44,13 @@ func NewAccountExperienceOpenCmd() *cobra.Command {
return cmdx.PrintOpenAPIError(cmd, err)
}

url := client.CloudAPIsURL(project.Slug)
url := client.CloudAPIsURL(project.Slug + ".projects")
url.Path = path.Join(url.Path, "ui", args[0])
if flagx.MustGetBool(cmd, cmdx.FlagQuiet) {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n", url)
return nil
}
if err := browser.OpenURL(url.String()); err != nil {
if err := h.OpenURL(url.String()); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n\nUnable to automatically open %s in your browser. Please open it manually!\n", err, url)
return cmdx.FailSilently(cmd)
}
Expand Down
22 changes: 17 additions & 5 deletions cmd/cloudx/accountexperience/accountexperience_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,39 @@
package accountexperience_test

import (
"context"
"strings"
"testing"

"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"

"github.com/ory/cli/cmd/cloudx/client"
"github.com/ory/cli/cmd/cloudx/testhelpers"
)

func TestMain(m *testing.M) {
testhelpers.RunAgainstStaging(m)
testhelpers.UseStaging()
m.Run()
}

func TestOpenAXPages(t *testing.T) {
cfg := testhelpers.NewConfigFile(t)
testhelpers.RegisterAccount(t, cfg)
project := testhelpers.CreateProject(t, cfg, nil)
cmd := testhelpers.CmdWithConfig(cfg)
_, _, _, sessionToken := testhelpers.RegisterAccount(context.Background(), t)
ctx := client.ContextWithOptions(context.Background(),
client.WithConfigLocation(testhelpers.NewConfigFile(t)),
client.WithSessionToken(t, sessionToken))
project := testhelpers.CreateProject(ctx, t, nil)
cmd := testhelpers.Cmd(ctx)

t.Run("is able to open all pages", func(t *testing.T) {
for _, flowType := range []string{"login", "registration", "recovery", "verification", "settings"} {
testhelpers.Cmd(client.ContextWithOptions(ctx, client.WithOpenBrowserHook(func(uri string) error {
assert.Truef(t, strings.HasPrefix(uri, "https://"+project.Slug), "expected %q to have prefix %q", uri, "https://"+project.Slug)
assert.Contains(t, uri, flowType)
return nil
})))

stdout, stderr, err := cmd.Exec(nil, "open", "account-experience", flowType, "--quiet")
require.NoError(t, err, stderr)
assert.Contains(t, stdout, "https://"+project.Slug)
Expand Down
195 changes: 9 additions & 186 deletions cmd/cloudx/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,206 +4,29 @@
package cloudx_test

import (
"bytes"
"context"
"io"
"os"
"testing"
"time"

"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ory/cli/cmd"
cloud "github.com/ory/client-go"

"github.com/ory/cli/cmd/cloudx/client"
"github.com/ory/cli/cmd/cloudx/testhelpers"
"github.com/ory/x/pointerx"
)

func TestAuthenticator(t *testing.T) {
configDir := testhelpers.NewConfigFile(t)
t.Parallel()

t.Run("errors without config and --quiet flag", func(t *testing.T) {
c := cmd.NewRootCmd()
c.SetArgs([]string{"auth", "--" + client.FlagConfig, configDir, "--quiet"})
require.Error(t, c.Execute())
})

password := testhelpers.FakePassword()
cmd := testhelpers.CmdWithConfigPassword(configDir, password)

signIn := func(t *testing.T, email string) (string, string, error) {
testhelpers.ClearConfig(t, configDir)
var r bytes.Buffer

_, _ = r.WriteString("y\n") // Do you want to sign in to an existing Ory Network account? [y/n]: y
_, _ = r.WriteString(email + "\n") // Email: FakeEmail()

return cmd.Exec(&r, "auth")
}

t.Run("success", func(t *testing.T) {
email := testhelpers.FakeEmail()
name := testhelpers.FakeName()

// Create the account
r := testhelpers.RegistrationBuffer(name, email)
stdout, stderr, err := cmd.Exec(r, "auth")
require.NoError(t, err)

assert.Contains(t, stderr, "You are now signed in as: "+email, "Expected to be signed in but response was:\n\t%s\n\tstderr: %s", stdout, stderr)
assert.Contains(t, stdout, email)
testhelpers.AssertConfig(t, configDir, email, name)
testhelpers.ClearConfig(t, configDir)

expectSignInSuccess := func(t *testing.T) {
stdout, _, err := signIn(t, email)
require.NoError(t, err)

assert.Contains(t, stderr, "You are now signed in as: ", email, stdout)
testhelpers.AssertConfig(t, configDir, email, name)
}

t.Run("sign in with valid data", func(t *testing.T) {
expectSignInSuccess(t)
})

t.Run("forced to reauthenticate on session expiration", func(t *testing.T) {
cmd := testhelpers.CmdWithConfig(configDir)
expectSignInSuccess(t)
testhelpers.ChangeAccessToken(t, configDir)
var r bytes.Buffer
r.WriteString("n\n") // Your CLI session has expired. Do you wish to login again as <email>?
_, stderr, err := cmd.Exec(&r, "list", "projects")
require.Error(t, err)
assert.Contains(t, stderr, "Your session has expired or has otherwise become invalid. Please re-authenticate to continue.")
})

t.Run("user is able to reauthenticate on session expiration", func(t *testing.T) {
cmd := testhelpers.CmdWithConfig(configDir)
expectSignInSuccess(t)
testhelpers.ChangeAccessToken(t, configDir)
var r bytes.Buffer
r.WriteString("y\n") // Your CLI session has expired. Do you wish to login again as <email>?
_, stderr, err := cmd.Exec(&r, "list", "projects")
require.Error(t, err)
assert.Contains(t, stderr, "Your session has expired or has otherwise become invalid. Please re-authenticate to continue.")
expectSignInSuccess(t)
})

t.Run("expired session with quiet flag returns error", func(t *testing.T) {
cmd := testhelpers.CmdWithConfig(configDir)
expectSignInSuccess(t)
testhelpers.ChangeAccessToken(t, configDir)
_, stderr, err := cmd.Exec(nil, "list", "projects", "-q")
require.Error(t, err)
assert.Equal(t, "please run `ory auth` to initialize your configuration or remove the `--quiet` flag", err.Error())
assert.NotContains(t, stderr, "Your session has expired or has otherwise become invalid. Please re-authenticate to continue.")
})

t.Run("set up 2fa", func(t *testing.T) {
expectSignInSuccess(t)
ac := testhelpers.ReadConfig(t, configDir)

c, err := client.NewOryProjectClient()
require.NoError(t, err)

flow, _, err := c.FrontendAPI.CreateNativeSettingsFlow(context.Background()).XSessionToken(ac.SessionToken).Execute()
require.NoError(t, err)

var secret string
for _, node := range flow.Ui.Nodes {
if node.Type != "text" {
continue
}

attrs := node.Attributes.UiNodeTextAttributes
if attrs.Text.Id == 1050006 {
secret = attrs.Text.Text
}
}

require.NotEmpty(t, secret)
code, err := totp.GenerateCode(secret, time.Now())
require.NoError(t, err)

_, _, err = c.FrontendAPI.UpdateSettingsFlow(context.Background()).XSessionToken(ac.SessionToken).Flow(flow.Id).UpdateSettingsFlowBody(cloud.UpdateSettingsFlowBody{
UpdateSettingsFlowWithTotpMethod: &cloud.UpdateSettingsFlowWithTotpMethod{
TotpCode: pointerx.Ptr(code),
Method: "totp",
},
}).Execute()
require.NoError(t, err)
testhelpers.ClearConfig(t, configDir)

t.Run("sign in fails because second factor is missing", func(t *testing.T) {
t.Skip("TODO")

testhelpers.ClearConfig(t, configDir)

var r bytes.Buffer

_, _ = r.WriteString("y\n") // Do you want to sign in to an existing Ory Network account? [y/n]: y
_, _ = r.WriteString(email + "\n") // Email: FakeEmail()

stdout, stderr, err := cmd.Exec(&r, "auth")
require.Error(t, err, stdout)

assert.Contains(t, stderr, "Please complete the second authentication challenge", stdout)
_, err = os.Stat(configDir)
assert.ErrorIs(t, err, os.ErrNotExist)
})

t.Run("sign in succeeds with second factor", func(t *testing.T) {
t.Skip("TODO")

testhelpers.ClearConfig(t, configDir)

var r bytes.Buffer

code, err := totp.GenerateCode(secret, time.Now())
require.NoError(t, err)
_, _ = r.WriteString("y\n") // Do you want to sign in to an existing Ory Network account? [y/n]: y
_, _ = r.WriteString(email + "\n") // Email: FakeEmail()
_, _ = r.WriteString(code + "\n") // TOTP code

stdout, stderr, err := cmd.Exec(&r, "auth")
require.NoError(t, err, stdout)

assert.Contains(t, stderr, "Please complete the second authentication challenge", stdout)
assert.Contains(t, stderr, "You are now signed in as: ", email, stdout)
testhelpers.AssertConfig(t, configDir, email, name)
})
})
ctx := testhelpers.WithCleanConfigFile(context.Background(), t)
_, _, err := testhelpers.Cmd(ctx).Exec(nil, "auth", "--quiet")
assert.ErrorIs(t, err, client.ErrNoConfigQuiet)
})

t.Run("retry sign up on invalid data", func(t *testing.T) {
testhelpers.ClearConfig(t, configDir)

r0 := testhelpers.RegistrationBuffer(testhelpers.FakeName(), "not-an-email")

// Redo the flow
email := testhelpers.FakeEmail()
name := testhelpers.FakeName()
r1 := testhelpers.RegistrationBuffer(name, email)
// on retry, we need to skip "Do you want to sign in to an existing Ory Network account? [y/n]: "
_, _ = r1.ReadString('\n')

stdout, stderr, err := cmd.Exec(io.MultiReader(r0, r1), "auth", "--"+client.FlagConfig, configDir)
require.NoError(t, err)

assert.Contains(t, stderr, "Your account creation attempt failed. Please try again!", stdout) // First try fails
assert.Contains(t, stderr, "You are now signed in as: "+email, stdout) // Second try succeeds
testhelpers.AssertConfig(t, configDir, email, name)
t.Run("triggers auth flow when not authenticated", func(t *testing.T) {
ctx := testhelpers.WithEmitAuthFlowTriggeredErr(context.Background(), t)
_, _, err := testhelpers.Cmd(ctx).Exec(nil, "auth")
assert.ErrorIs(t, err, testhelpers.ErrAuthFlowTriggered)
})

t.Run("sign in with invalid data", func(t *testing.T) {
stdout, stderr, err := signIn(t, testhelpers.FakeEmail())
require.Error(t, err, stdout)

assert.Contains(t, stderr, "The provided credentials are invalid", stdout)
})
// the full e2e flow is tested on the internal helper function instead of the full CLI wrapper
}
Loading

0 comments on commit cf39d4a

Please sign in to comment.