Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions tavern/cli/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ func Authenticate(ctx context.Context, browser Browser, tavernURL string, opts .
case err := <-errCh:
return Token(""), fmt.Errorf("failed to obtain credentials: %w", err)
case token := <-tokenCh:
if options.CachePath != "" {
err := os.WriteFile(options.CachePath, []byte(token), 0600)
if err != nil {
slog.Warn("failed to write credential cache", "path", options.CachePath, "error", err)
}
}
return token, nil
}
}
Expand Down
3 changes: 3 additions & 0 deletions tavern/cli/auth/rda.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"io"
"net/http"
"time"

"realm.pub/tavern/cli/qrcode"
)

// AuthenticateRemoteDevice begins the remote device authentication flow by polling
Expand Down Expand Up @@ -45,6 +47,7 @@ func AuthenticateRemoteDevice(ctx context.Context, tavernURL string, tokenCh cha
}

fmt.Printf("\n\nOpen %s on any device and enter the following code:\n\n\t%s\n\nWaiting for approval...\n\n", codeRes.VerificationURI, codeRes.UserCode)
qrcode.PrintQRCode(codeRes.VerificationURIComplete)

// 2. Poll for Token
interval := time.Duration(codeRes.Interval) * time.Second
Expand Down
17 changes: 17 additions & 0 deletions tavern/cli/qrcode/qrcode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package qrcode

import (
"fmt"

qrcode "github.com/skip2/go-qrcode"
)

// PrintQRCode prints a URL as a scannable ASCII art QR code to standard output
func PrintQRCode(url string) error {
qr, err := qrcode.New(url, qrcode.Medium)
if err != nil {
return err
}
fmt.Println(qr.ToSmallString(false))
return nil
}
22 changes: 16 additions & 6 deletions tavern/internal/http/device_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ func generateDeviceCode() string {
}

type RDACodeResponse struct {
UserCode string `json:"user_code"`
DeviceCode string `json:"device_code"`
ExpiresIn int `json:"expires_in"` // seconds
UserCode string `json:"user_code"`
DeviceCode string `json:"device_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresIn int `json:"expires_in"` // seconds
}

func NewRDACodeHandler(client *ent.Client) http.HandlerFunc {
Expand All @@ -54,10 +56,18 @@ func NewRDACodeHandler(client *ent.Client) http.HandlerFunc {
return
}

scheme := "http"
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
scheme = "https"
}
verificationURI := fmt.Sprintf("%s://%s/profile", scheme, r.Host)

resp := RDACodeResponse{
UserCode: userCode,
DeviceCode: deviceCode,
ExpiresIn: 600,
UserCode: userCode,
DeviceCode: deviceCode,
VerificationURI: verificationURI,
VerificationURIComplete: fmt.Sprintf("%s?device-code=%s", verificationURI, userCode),
ExpiresIn: 600,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
Expand Down
15 changes: 12 additions & 3 deletions tavern/internal/www/src/pages/profile/DeviceAuth.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { useToast, Input } from '@chakra-ui/react';
import { createColumnHelper } from '@tanstack/react-table';
import Button from '../../components/tavern-base-ui/button/Button';
import Table from '../../components/tavern-base-ui/table/Table';
import { EmptyState, EmptyStateType } from '../../components/tavern-base-ui/EmptyState';
import { DeviceAuthNode } from './Profile';
import { Trash2 } from 'lucide-react';
import { useLocation } from 'react-router-dom';

interface DeviceAuthProps {
devices: { node: DeviceAuthNode }[];
Expand All @@ -16,6 +17,14 @@ const DeviceAuth: React.FC<DeviceAuthProps> = ({ devices, refetch }) => {
const toast = useToast();
const [userCode, setUserCode] = useState('');
const [approving, setApproving] = useState(false);
const location = useLocation();

useEffect(() => {
const searchParams = new URLSearchParams(location.search);
if (searchParams.has('device-code')) {
setUserCode(searchParams.get('device-code') || '');
}
}, [location.search]);

const handleApproveDevice = async () => {
if (!userCode.trim()) return;
Expand Down Expand Up @@ -79,7 +88,7 @@ const DeviceAuth: React.FC<DeviceAuthProps> = ({ devices, refetch }) => {
if (node.status === 'PENDING' || node.status === 'APPROVED') {
return (
<div className="flex justify-end">
<Button buttonVariant="outline" buttonStyle={{ color: 'red', size: 'xs' }} onClick={() => handleRevokeDevice(node.id)}>
<Button buttonVariant="outline" buttonStyle={{ color: 'red', size: 'xs' }} onClick={() => handleRevokeDevice(node.userCode)}>
<Trash2 size={16} />
</Button>
</div>
Expand All @@ -88,7 +97,7 @@ const DeviceAuth: React.FC<DeviceAuthProps> = ({ devices, refetch }) => {
return null;
}
})
], [columnHelper]);
], [columnHelper, handleRevokeDevice]);

return (
<div className="bg-white p-6 shadow-sm rounded-md border border-gray-200">
Expand Down
Loading