Merge pull request #1414 from jonnybarnes/1384-implement-auth-endpoint

Implement IndieAuth endpoint
This commit is contained in:
Jonny Barnes 2024-06-08 19:45:36 +01:00 • committed by GitHub
commit a440533a76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2068 additions and 804 deletions

View file

@ -19,7 +19,6 @@ use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Throwable;
use Webauthn\AttestationStatement\AttestationObjectLoader;
use Webauthn\AttestationStatement\AttestationStatementSupportManager;
use Webauthn\AttestationStatement\NoneAttestationStatementSupport;
use Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler;
@ -28,9 +27,11 @@ use Webauthn\AuthenticatorAssertionResponseValidator;
use Webauthn\AuthenticatorAttestationResponse;
use Webauthn\AuthenticatorAttestationResponseValidator;
use Webauthn\AuthenticatorSelectionCriteria;
use Webauthn\CeremonyStep\CeremonyStepManagerFactory;
use Webauthn\Denormalizer\WebauthnSerializerFactory;
use Webauthn\Exception\WebauthnException;
use Webauthn\PublicKeyCredential;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialLoader;
use Webauthn\PublicKeyCredentialParameters;
use Webauthn\PublicKeyCredentialRequestOptions;
use Webauthn\PublicKeyCredentialRpEntity;
@ -109,39 +110,57 @@ class PasskeysController extends Controller
$user = auth()->user();
$publicKeyCredentialCreationOptionsData = session('create_options');
// Unset session data to mitigate replay attacks
session()->forget('create_options');
if (empty($publicKeyCredentialCreationOptionsData)) {
throw new WebAuthnException('No public key credential request options found');
}
$publicKeyCredentialCreationOptions = PublicKeyCredentialCreationOptions::createFromString($publicKeyCredentialCreationOptionsData);
// Unset session data to mitigate replay attacks
session()->forget('create_options');
$attestationStatementSupportManager = new AttestationStatementSupportManager();
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
$attestationSupportManager = AttestationStatementSupportManager::create();
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
$webauthnSerializer = (new WebauthnSerializerFactory(
$attestationStatementSupportManager
))->create();
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
$publicKeyCredential = $webauthnSerializer->deserialize(
json_encode($request->all(), JSON_THROW_ON_ERROR),
PublicKeyCredential::class,
'json'
);
if (! $publicKeyCredential->response instanceof AuthenticatorAttestationResponse) {
throw new WebAuthnException('Invalid response type');
}
$attestationStatementSupportManager = AttestationStatementSupportManager::create();
$attestationStatementSupportManager->add(NoneAttestationStatementSupport::create());
$algorithmManager = new Manager();
$algorithmManager->add(new Ed25519());
$algorithmManager->add(new ES256());
$algorithmManager->add(new RS256());
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
attestationStatementSupportManager: $attestationStatementSupportManager,
publicKeyCredentialSourceRepository: null,
tokenBindingHandler: null,
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
$ceremonyStepManagerFactory = new CeremonyStepManagerFactory();
$ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager);
$ceremonyStepManagerFactory->setAttestationStatementSupportManager(
$attestationStatementSupportManager
);
$ceremonyStepManagerFactory->setExtensionOutputCheckerHandler(
ExtensionOutputCheckerHandler::create()
);
$securedRelyingPartyId = [];
if (App::environment('local', 'development')) {
$securedRelyingPartyId = [config('url.longurl')];
}
$ceremonyStepManagerFactory->setSecuredRelyingPartyId($securedRelyingPartyId);
$authenticatorAttestationResponseValidator = AuthenticatorAttestationResponseValidator::create(
ceremonyStepManager: $ceremonyStepManagerFactory->creationCeremony()
);
$publicKeyCredentialCreationOptions = $webauthnSerializer->deserialize(
$publicKeyCredentialCreationOptionsData,
PublicKeyCredentialCreationOptions::class,
'json'
);
$publicKeyCredentialSource = $authenticatorAttestationResponseValidator->check(
authenticatorAttestationResponse: $publicKeyCredential->response,
@ -187,14 +206,18 @@ class PasskeysController extends Controller
], 400);
}
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($requestOptions);
$attestationStatementSupportManager = new AttestationStatementSupportManager();
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
$attestationSupportManager = AttestationStatementSupportManager::create();
$attestationSupportManager->add(NoneAttestationStatementSupport::create());
$attestationObjectLoader = AttestationObjectLoader::create($attestationSupportManager);
$publicKeyCredentialLoader = PublicKeyCredentialLoader::create($attestationObjectLoader);
$webauthnSerializer = (new WebauthnSerializerFactory(
$attestationStatementSupportManager
))->create();
$publicKeyCredential = $publicKeyCredentialLoader->load(json_encode($request->all(), JSON_THROW_ON_ERROR));
$publicKeyCredential = $webauthnSerializer->deserialize(
json_encode($request->all(), JSON_THROW_ON_ERROR),
PublicKeyCredential::class,
'json'
);
if (! $publicKeyCredential->response instanceof AuthenticatorAssertionResponse) {
return response()->json([
@ -211,28 +234,47 @@ class PasskeysController extends Controller
], 404);
}
$credential = PublicKeyCredentialSource::createFromArray(json_decode($passkey->passkey, true, 512, JSON_THROW_ON_ERROR));
$publicKeyCredentialSource = $webauthnSerializer->deserialize(
$passkey->passkey,
PublicKeyCredentialSource::class,
'json'
);
$algorithmManager = Manager::create();
$algorithmManager = new Manager();
$algorithmManager->add(new Ed25519());
$algorithmManager->add(new ES256());
$algorithmManager->add(new RS256());
$authenticatorAssertionResponseValidator = new AuthenticatorAssertionResponseValidator(
publicKeyCredentialSourceRepository: null,
tokenBindingHandler: null,
extensionOutputCheckerHandler: ExtensionOutputCheckerHandler::create(),
algorithmManager: $algorithmManager,
);
$attestationStatementSupportManager = new AttestationStatementSupportManager();
$attestationStatementSupportManager->add(new NoneAttestationStatementSupport());
$ceremonyStepManagerFactory = new CeremonyStepManagerFactory();
$ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager);
$ceremonyStepManagerFactory->setAttestationStatementSupportManager(
$attestationStatementSupportManager
);
$ceremonyStepManagerFactory->setExtensionOutputCheckerHandler(
ExtensionOutputCheckerHandler::create()
);
$securedRelyingPartyId = [];
if (App::environment('local', 'development')) {
$securedRelyingPartyId = [config('url.longurl')];
}
$ceremonyStepManagerFactory->setSecuredRelyingPartyId($securedRelyingPartyId);
$authenticatorAssertionResponseValidator = AuthenticatorAssertionResponseValidator::create(
ceremonyStepManager: $ceremonyStepManagerFactory->requestCeremony()
);
$publicKeyCredentialRequestOptions = $webauthnSerializer->deserialize(
$requestOptions,
PublicKeyCredentialRequestOptions::class,
'json'
);
try {
$authenticatorAssertionResponseValidator->check(
credentialId: $credential,
credentialId: $publicKeyCredentialSource,
authenticatorAssertionResponse: $publicKeyCredential->response,
publicKeyCredentialRequestOptions: $publicKeyCredentialRequestOptions,
request: config('url.longurl'),

View file

@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Services\TokenService;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Uri;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
use Illuminate\View\View;
use Random\RandomException;
use SodiumException;
class IndieAuthController extends Controller
{
public function indieAuthMetadataEndpoint(): JsonResponse
{
return response()->json([
'issuer' => config('app.url'),
'authorization_endpoint' => route('indieauth.start'),
'token_endpoint' => route('indieauth.token'),
'code_challenge_methods_supported' => ['S256'],
//'introspection_endpoint' => route('indieauth.introspection'),
//'introspection_endpoint_auth_methods_supported' => ['none'],
]);
}
/**
* Process a GET request to the IndieAuth endpoint.
*
* This is the first step in the IndieAuth flow, where the client app sends the user to the IndieAuth endpoint.
*/
public function start(Request $request): View
{
// First check all required params are present
$validator = Validator::make($request->all(), [
'response_type' => 'required:string',
'client_id' => 'required',
'redirect_uri' => 'required',
'state' => 'required',
'code_challenge' => 'required:string',
'code_challenge_method' => 'required:string',
], [
'response_type' => 'response_type is required',
'client_id.required' => 'client_id is required to display which app is asking for authentication',
'redirect_uri.required' => 'redirect_uri is required so we can progress successful requests',
'state.required' => 'state is required',
'code_challenge.required' => 'code_challenge is required',
'code_challenge_method.required' => 'code_challenge_method is required',
]);
if ($validator->fails()) {
return view('indieauth.error')->withErrors($validator);
}
if ($request->get('response_type') !== 'code') {
return view('indieauth.error')->withErrors(['response_type' => 'only a response_type of "code" is supported']);
}
if (mb_strtoupper($request->get('code_challenge_method')) !== 'S256') {
return view('indieauth.error')->withErrors(['code_challenge_method' => 'only a code_challenge_method of "S256" is supported']);
}
if (! $this->isValidRedirectUri($request->get('client_id'), $request->get('redirect_uri'))) {
return view('indieauth.error')->withErrors(['redirect_uri' => 'redirect_uri is not valid for this client_id']);
}
$scopes = $request->get('scopes', '');
$scopes = explode(' ', $scopes);
return view('indieauth.start', [
'me' => $request->get('me'),
'client_id' => $request->get('client_id'),
'redirect_uri' => $request->get('redirect_uri'),
'state' => $request->get('state'),
'scopes' => $scopes,
'code_challenge' => $request->get('code_challenge'),
'code_challenge_method' => $request->get('code_challenge_method'),
]);
}
/**
* Confirm an IndieAuth approval request.
*
* Generates an auth code and redirects the user back to the client app.
*
* @throws RandomException
*/
public function confirm(Request $request): RedirectResponse
{
$authCode = bin2hex(random_bytes(16));
$cacheKey = hash('xxh3', $request->get('client_id'));
$indieAuthRequestData = [
'code_challenge' => $request->get('code_challenge'),
'code_challenge_method' => $request->get('code_challenge_method'),
'client_id' => $request->get('client_id'),
'redirect_uri' => $request->get('redirect_uri'),
'auth_code' => $authCode,
'scopes' => $request->get('scopes', ''),
];
Cache::put($cacheKey, $indieAuthRequestData, now()->addMinutes(10));
$redirectUri = new Uri($request->get('redirect_uri'));
$redirectUri = Uri::withQueryValues($redirectUri, [
'code' => $authCode,
'state' => $request->get('state'),
'iss' => config('app.url'),
]);
// For now just dump URL scheme
// return response()->json([
// 'redirect_uri' => $redirectUri,
// ]);
return redirect()->away($redirectUri);
}
/**
* Process a POST request to the IndieAuth auth endpoint.
*
* This is one possible second step in the IndieAuth flow, where the client app sends the auth code to the IndieAuth
* endpoint. As it is to the auth endpoint we return profile information. A similar request can be made to the token
* endpoint to get an access token.
*
* @throws SodiumException
*/
public function processCodeExchange(Request $request): JsonResponse
{
$invalidCodeResponse = $this->validateAuthorizationCode($request);
if ($invalidCodeResponse instanceof JsonResponse) {
return $invalidCodeResponse;
}
return response()->json([
'me' => config('app.url'),
]);
}
/**
* Process a POST request to the IndieAuth token endpoint.
*
* This is another possible second step in the IndieAuth flow, where the client app sends the auth code to the
* IndieAuth token endpoint. As it is to the token endpoint we return an access token.
*
* @throws SodiumException
*/
public function processTokenRequest(Request $request): JsonResponse
{
$indieAuthData = $this->validateAuthorizationCode($request);
if ($indieAuthData instanceof JsonResponse) {
return $indieAuthData;
}
if ($indieAuthData['scopes'] === '') {
return response()->json(['errors' => [
'scope' => [
'The scope property must be non-empty for an access token to be issued.',
],
]], 400);
}
$tokenData = [
'me' => config('app.url'),
'client_id' => $request->get('client_id'),
'scope' => $indieAuthData['scopes'],
];
$tokenService = resolve(TokenService::class);
$token = $tokenService->getNewToken($tokenData);
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'scope' => $indieAuthData['scopes'],
'me' => config('app.url'),
]);
}
protected function isValidRedirectUri(string $clientId, string $redirectUri): bool
{
// If client_id is not a valid URL, then it's not valid
$clientIdParsed = \Mf2\parseUriToComponents($clientId);
if (! isset($clientIdParsed['authority'])) {
ray($clientIdParsed);
return false;
}
// If redirect_uri is not a valid URL, then it's not valid
$redirectUriParsed = \Mf2\parseUriToComponents($redirectUri);
if (! isset($redirectUriParsed['authority'])) {
ray($redirectUriParsed);
return false;
}
// If client_id and redirect_uri are the same host, then it's valid
if ($clientIdParsed['authority'] === $redirectUriParsed['authority']) {
return true;
}
// Otherwise we need to check the redirect_uri is in the client_id's redirect_uris
$guzzle = resolve(Client::class);
try {
$clientInfo = $guzzle->get($clientId);
} catch (Exception $e) {
ray('Failed to fetch client info', $e->getMessage());
return false;
}
$clientInfoParsed = \Mf2\parse($clientInfo->getBody()->getContents(), $clientId);
$redirectUris = $clientInfoParsed['rels']['redirect_uri'] ?? [];
return in_array($redirectUri, $redirectUris, true);
}
protected function validateAuthorizationCode(Request $request): JsonResponse|array
{
// First check all the data is present
$validator = Validator::make($request->all(), [
'grant_type' => 'required:string',
'code' => 'required:string',
'client_id' => 'required',
'redirect_uri' => 'required',
'code_verifier' => 'required',
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 400);
}
if ($request->get('grant_type') !== 'authorization_code') {
return response()->json(['errors' => [
'grant_type' => [
'Only a grant type of "authorization_code" is supported.',
],
]], 400);
}
// Check cache for auth code
$cacheKey = hash('xxh3', $request->get('client_id'));
$indieAuthRequestData = Cache::pull($cacheKey);
if ($indieAuthRequestData === null) {
return response()->json(['errors' => [
'code' => [
'The code is invalid.',
],
]], 404);
}
// Check the IndieAuth code
if (! array_key_exists('auth_code', $indieAuthRequestData)) {
return response()->json(['errors' => [
'code' => [
'The code is invalid.',
],
]], 400);
}
if ($indieAuthRequestData['auth_code'] !== $request->get('code')) {
return response()->json(['errors' => [
'code' => [
'The code is invalid.',
],
]], 400);
}
// Check code verifier
if (! array_key_exists('code_challenge', $indieAuthRequestData)) {
return response()->json(['errors' => [
'code_verifier' => [
'The code verifier is invalid.',
],
]], 400);
}
if (! hash_equals(
$indieAuthRequestData['code_challenge'],
sodium_bin2base64(
hash('sha256', $request->get('code_verifier'), true),
SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING
)
)) {
return response()->json(['errors' => [
'code_verifier' => [
'The code verifier is invalid.',
],
]], 400);
}
// Check redirect_uri
if (! array_key_exists('redirect_uri', $indieAuthRequestData)) {
return response()->json(['errors' => [
'redirect_uri' => [
'The redirect uri is invalid.',
],
]], 400);
}
if ($indieAuthRequestData['redirect_uri'] !== $request->get('redirect_uri')) {
return response()->json(['errors' => [
'redirect_uri' => [
'The redirect uri is invalid.',
],
]], 400);
}
// Check client_id
if (! array_key_exists('client_id', $indieAuthRequestData)) {
return response()->json(['errors' => [
'client_id' => [
'The client id is invalid.',
],
]], 400);
}
if ($indieAuthRequestData['client_id'] !== $request->get('client_id')) {
return response()->json(['errors' => [
'client_id' => [
'The client id is invalid.',
],
]], 400);
}
return $indieAuthRequestData;
}
}

View file

@ -1,109 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Services\TokenService;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use IndieAuth\Client;
use JsonException;
/**
* @psalm-suppress UnusedClass
*/
class TokenEndpointController extends Controller
{
/**
* @var Client The IndieAuth Client.
*/
protected Client $client;
/**
* @var GuzzleClient The GuzzleHttp client.
*/
protected GuzzleClient $guzzle;
protected TokenService $tokenService;
/**
* Inject the dependencies.
*/
public function __construct(
Client $client,
GuzzleClient $guzzle,
TokenService $tokenService
) {
$this->client = $client;
$this->guzzle = $guzzle;
$this->tokenService = $tokenService;
}
/**
* If the user has auth’d via the IndieAuth protocol, issue a valid token.
*/
public function create(Request $request): JsonResponse
{
$auth = $this->verifyIndieAuthCode(
config('url.authorization_endpoint'),
$request->input('code'),
$request->input('redirect_uri'),
$request->input('client_id'),
);
if ($auth === null || ! array_key_exists('me', $auth)) {
return response()->json([
'error' => 'There was an error verifying the IndieAuth code',
], 401);
}
$scope = $auth['scope'] ?? '';
$tokenData = [
'me' => config('app.url'),
'client_id' => $request->input('client_id'),
'scope' => $scope,
];
$token = $this->tokenService->getNewToken($tokenData);
$content = [
'me' => config('app.url'),
'scope' => $scope,
'access_token' => $token,
];
return response()->json($content);
}
protected function verifyIndieAuthCode(
string $authorizationEndpoint,
string $code,
string $redirectUri,
string $clientId
): ?array {
try {
$response = $this->guzzle->request('POST', $authorizationEndpoint, [
'headers' => [
'Accept' => 'application/json',
],
'form_params' => [
'code' => $code,
'me' => config('app.url'),
'redirect_uri' => $redirectUri,
'client_id' => $clientId,
],
]);
} catch (BadResponseException) {
return null;
}
try {
$authData = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return null;
}
return $authData;
}
}

View file

@ -16,10 +16,11 @@ class LinkHeadersMiddleware
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->header('Link', '<https://indieauth.com/auth>; rel="authorization_endpoint"', false);
$response->header('Link', '<' . config('app.url') . '/api/token>; rel="token_endpoint"', false);
$response->header('Link', '<' . config('app.url') . '/api/post>; rel="micropub"', false);
$response->header('Link', '<' . config('app.url') . '/webmention>; rel="webmention"', false);
$response->header('Link', '<' . route('indieauth.metadata') . '>; rel="indieauth-metadata"', false);
$response->header('Link', '<' . route('indieauth.start') . '>; rel="authorization_endpoint"', false);
$response->header('Link', '<' . route('indieauth.token') . '>; rel="token_endpoint"', false);
$response->header('Link', '<' . route('micropub-endpoint') . '>; rel="micropub"', false);
$response->header('Link', '<' . route('webmention-endpoint') . '>; rel="webmention"', false);
return $response;
}

View file

@ -20,6 +20,8 @@ class MyAuthMiddleware
{
if (Auth::check() === false) {
// they’re not logged in, so send them to login form
redirect()->setIntendedUrl($request->url());
return redirect()->route('login');
}

View file

@ -1,5 +1,6 @@
<?php
use App\Http\Middleware\LinkHeadersMiddleware;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
@ -11,13 +12,16 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'api/token',
'api/post',
'api/media',
'micropub/places',
'webmention',
]);
$middleware
->append(LinkHeadersMiddleware::class)
->validateCsrfTokens(except: [
'auth', // This is the IndieAuth auth endpoint
'token', // This is the IndieAuth token endpoint
'api/post',
'api/media',
'micropub/places',
'webmention',
]);
})
->withExceptions(function (Exceptions $exceptions) {
//

View file

@ -10,6 +10,7 @@
"ext-intl": "*",
"ext-json": "*",
"ext-pgsql": "*",
"ext-sodium": "*",
"cviebrock/eloquent-sluggable": "^11.0",
"guzzlehttp/guzzle": "^7.2",
"indieauth/client": "^1.1",
@ -26,9 +27,12 @@
"league/commonmark": "^2.0",
"league/flysystem-aws-s3-v3": "^3.0",
"mf2/mf2": "~0.3",
"phpdocumentor/reflection-docblock": "^5.3",
"spatie/commonmark-highlighter": "^3.0",
"spatie/laravel-ignition": "^2.1",
"symfony/html-sanitizer": "^7.0",
"symfony/property-access": "^7.0",
"symfony/serializer": "^7.0",
"web-auth/webauthn-lib": "^4.7"
},
"require-dev": {

1146
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -29,15 +29,4 @@ return [
'shorturl' => env('APP_SHORTURL', 'shorturl.local'),
/*
|--------------------------------------------------------------------------
| Authorization endpoint
|--------------------------------------------------------------------------
|
| The authorization endpoint for the application, used primarily for Micropub
|
*/
'authorization_endpoint' => env('AUTHORIZATION_ENDPOINT', 'https://indieauth.com/auth'),
];

View file

@ -1,5 +1,4 @@
# For more information: https://laravel.com/docs/sail
version: '3'
services:
laravel.test:
build:
@ -18,6 +17,7 @@ services:
LARAVEL_SAIL: 1
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
volumes:
- '.:/var/www/html'
networks:

View file

@ -5,3 +5,4 @@
@import url('code.css');
@import url('content.css');
@import url('notes.css');
@import url('indieauth.css');

Binary file not shown.

View file

@ -0,0 +1,15 @@
.indieauth {
.error {
color: var(--color-danger);
background-color: var(--color-danger-shadow);
border: 1px solid var(--color-danger);
border-radius: .5rem;
display: flex;
padding-inline: 1rem;
padding-block: .5rem;
width: fit-content;
margin-block-end: 1rem;
}
}

Binary file not shown.

View file

@ -20,4 +20,6 @@
--color-link-visited: oklch(70.44% 0.21 304.41deg);
--color-primary-shadow: oklch(19.56% 0.054 125.505deg / 40%);
--rss-color-link: oklch(67.59% 0.189 42.04deg);
--color-danger: oklch(64.41% 0.281 23.29deg);
--color-danger-shadow: oklch(64.41% 0.281 23.29deg / 10%);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,12 @@
@extends('master')
@section('title')IndieAuth « @stop
@section('content')
<section class="indieauth">
<h1>IndieAuth</h1>
@foreach ($errors->all() as $message)
<div class="error">{{ $message }}</div>
@endforeach
</section>
@stop

View file

@ -0,0 +1,39 @@
@extends('master')
@section('title')IndieAuth « @stop
@section('content')
<form class="indieauth" action="/auth/confirm" method="post">
@csrf
<input type="hidden" name="client_id" value="{{ $client_id }}">
<input type="hidden" name="redirect_uri" value="{{ $redirect_uri }}">
<input type="hidden" name="state" value="{{ $state }}">
<input type="hidden" name="me" value="{{ $me }}">
<input type="hidden" name="code_challenge" value="{{ $code_challenge }}">
<input type="hidden" name="code_challenge_method" value="{{ $code_challenge_method }}">
@if(!empty($scopes))
@foreach($scopes as $scope)
<input type="hidden" name="scope[]" value="{{ $scope }}">
@endforeach
@endif
<h1>IndieAuth</h1>
@if(!empty($error))
<div class="error">{{ $error }}</div>
@endif
<p>You are attempting to log in with the client <code>{{ $client_id }}</code></p>
<p>After approving the request you will be redirected to <code>{{ $redirect_uri }}</code></p>
@if(!empty($scopes))
<p>The client is requesting the following scopes:</p>
<ul>
@foreach($scopes as $scope)
<li>{{ $scope }}</li>
@endforeach
</ul>
@endif
<button type="submit">Approve</button>
</form>
@stop

Some files were not shown because too many files have changed in this diff Show more