Skip to content

Commit

Permalink
staging Implement social auth api and refactor code base
Browse files Browse the repository at this point in the history
  • Loading branch information
latheesan-k committed Nov 15, 2024
1 parent b788cb3 commit 64eb680
Show file tree
Hide file tree
Showing 20 changed files with 708 additions and 60 deletions.
16 changes: 16 additions & 0 deletions application/app/Enums/AuthProviderType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\Enums;

use App\Traits\EnumToArrayTrait;

enum AuthProviderType: string
{
use EnumToArrayTrait;

case WALLET = 'wallet';
case GOOGLE = 'google';
case TWITTER = 'twitter';
case DISCORD = 'discord';
case GITHUB = 'github';
}
144 changes: 144 additions & 0 deletions application/app/Http/Controllers/API/AuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

namespace App\Http\Controllers\API;

use App\Enums\AuthProviderType;
use App\Http\Controllers\Controller;
use App\Models\Project;
use App\Models\ProjectAccount;
use App\Traits\GEOBlockTrait;
use App\Traits\IPHelperTrait;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Laravel\Socialite\Facades\Socialite;
use Throwable;

class AuthController extends Controller
{
use IPHelperTrait, GEOBlockTrait;

public function providers(): JsonResponse
{
// Return supported auth providers
return response()
->json(AuthProviderType::values());
}

public function init(string $publicApiKey, string $authProvider, Request $request): RedirectResponse|JsonResponse
{
// Validate requested auth provider
if (!in_array($authProvider, AuthProviderType::values(), true)) {
return response()->json([
'error' => __('Bad Request'),
'reason' => __(':currentProvider provider is not valid, supported providers are: :supportedProviders', [
'currentProvider' => $authProvider,
'supportedProviders' => implode(', ', AuthProviderType::values()),
]),
], 400);
}

// Check if reference is provided in the request
if (empty($request->get('reference'))) {
return response()->json([
'error' => __('Bad Request'),
'reason' => __('The reference query string parameter is missing or empty'),
], 400);
}

// Load project by public api key
$project = Project::query()
->where('public_api_key', $publicApiKey)
->first();

// Check if project exists
if (!$project) {
return response()->json([
'error' => __('Unauthorized'),
'reason' => __('Invalid project public api key'),
], 401);
}

// Check if this request should be geo-blocked
if ($this->isGEOBlocked($project, $request)) {
return response()->json([
'error' => __('Unauthorized'),
'reason' => __('Accept not permitted'),
], 401);
}

// Setup project account
$projectAccount = ProjectAccount::query()
->where('project_id', $project->id)
->where('reference', $request->get('reference'))
->where('auth_provider', $authProvider)
->first();
if (!$projectAccount) {
$projectAccount = new ProjectAccount;
$projectAccount->fill([
'project_id' => $project->id,
'reference' => $request->get('reference'),
'auth_provider' => $authProvider,
]);
$projectAccount->save();
}

// Attach project account to hashed user ip in cache for 5 minutes (needed during social auth callback)
Cache::put(
sprintf('project-account:%s', md5($this->getIP($request))),
$projectAccount->id,
300,
);

// TODO: Handle wallet auth differently

// Redirect to social auth
return Socialite::driver($authProvider)
->redirect();
}

public function check(Request $request): JsonResponse
{
try {

// Load project account by reference
$projectAccount = ProjectAccount::query()
->where('reference', $request->get('reference'))
->first();
if (!$projectAccount) {
return response()->json([
'error' => __('Not Found'),
'reason' => __('Could not find project account by reference'),
], 404);
}

// Success
return response()->json([
'reference' => $projectAccount->reference,
'auth_provider' => $projectAccount->auth_provider,
'auth_provider_id' => $projectAccount->auth_provider_id,
'auth_name' => $projectAccount->auth_name,
'auth_email' => $projectAccount->auth_email,
'auth_avatar' => $projectAccount->auth_avatar,
'auth_country_code' => $projectAccount->auth_country_code,
'authenticated_at' => $projectAccount->authenticated_at->toDateTimeString(),
'is_authenticated' => !empty($projectAccount->authenticated_at),
]);

} catch (Throwable $exception) {

// Log exception
$this->logException('Failed to handle auth check', $exception, [
'request' => $request->toArray(),
]);

// Handle error
return response()->json([
'error' => __('Internal Server Error'),
'reason' => __('An unknown error occurred, please notify server administrator'),
], 500);

}
}
}
35 changes: 32 additions & 3 deletions application/app/Http/Controllers/ProjectsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,43 @@ public function store(StoreProjectRequest $request): RedirectResponse
$project->fill([
'user_id' => auth()->id(),
'name' => $request->validated('name'),
'public_api_key' => encrypt(Str::uuid()),
'private_api_key' => encrypt(Str::uuid()),
'public_api_key' => Str::uuid(),
'private_api_key' => Str::uuid(),
'geo_blocked_countries' => $request->validated('geo_blocked_countries'),
]);
$project->save();

return redirect()
->route('projects.index')
->route('projects.show', $project)
->with('alert', __('New project (:name) successfully created.', ['name' => $request->validated('name')]));
}

public function update(int $projectId, StoreProjectRequest $request): RedirectResponse
{
$project = Project::query()
->where('user_id', auth()->id())
->where('id', $projectId)
->first();

if (!$project) {
return redirect()
->route('projects.index')
->with('alert', __('Project not found.'));
}

$changes = [
'name' => $request->validated('name'),
'geo_blocked_countries' => $request->validated('geo_blocked_countries'),
];
if ($request->validated('regenerate_api_keys') === 'yes') {
$changes['public_api_key'] = Str::uuid();
$changes['private_api_key'] = Str::uuid();
}

$project->update($changes);

return redirect()
->route('projects.show', $project)
->with('alert', __('Project (:name) successfully updated.', ['name' => $request->validated('name')]));
}
}
81 changes: 81 additions & 0 deletions application/app/Http/Controllers/SocialAuthCallbackController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace App\Http\Controllers;

use App\Enums\AuthProviderType;
use App\Models\ProjectAccount;
use App\Traits\GEOBlockTrait;
use App\Traits\IPHelperTrait;
use App\Traits\LogExceptionTrait;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Laravel\Socialite\Facades\Socialite;
use Throwable;

class SocialAuthCallbackController extends Controller
{
use LogExceptionTrait, IPHelperTrait, GEOBlockTrait;

public function handle(string $authProvider, Request $request): void
{
try {

// Validate requested auth provider
if (!in_array($authProvider, AuthProviderType::values(), true)) {
exit(__(':currentProvider provider is not valid, supported providers are: :supportedProviders', [
'currentProvider' => $authProvider,
'supportedProviders' => implode(', ', AuthProviderType::values()),
]));
}

// Retrieve project account
$projectAccountId = Cache::get(sprintf('project-account:%s', md5($this->getIP($request))));
if (empty($projectAccountId)) {
exit(__('Login attempt session expired, please try again'));
}
$projectAccount = ProjectAccount::query()
->where('id', $projectAccountId)
->with('project')
->first();
if (!$projectAccount) {
exit(__('Login attempt session not found, please try again'));
}

// Check if this request should be geo-blocked
if ($this->isGEOBlocked($projectAccount->project, $request)) {
exit(__('Accept not permitted'));
}

// Load social user
$socialUser = null;
try {
$socialUser = Socialite::driver($authProvider)->user();
} catch (Throwable) {}
if (!$socialUser) {
exit(__('Failed to authenticate via :authProvider, please try again', ['authProvider' => $authProvider]));
}

// Update project account with social account info
$projectAccount->update([
'auth_provider_id' => $socialUser->id,
'auth_name' => $socialUser->getName(),
'auth_email' => $socialUser->getEmail(),
'auth_avatar' => $socialUser->getAvatar(),
'auth_country_code' => $this->getIPCountryCode($request),
'authenticated_at' => now(),
]);

// Success
exit(__('You have successfully logged in via :authProvider', ['authProvider' => $authProvider]));

} catch (Throwable $exception) {

// Handle exception
$this->logException('Failed to handle social auth callback', $exception);

// Display generic error message
exit(__('Failed to authenticate via :authProvider, please try again', ['authProvider' => $authProvider]));

}
}
}
35 changes: 35 additions & 0 deletions application/app/Http/Middleware/ProjectAPIKeyAuth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace App\Http\Middleware;

use App\Models\Project;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ProjectAPIKeyAuth
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return Response|JsonResponse
*/
public function handle(Request $request, Closure $next): Response|JsonResponse
{
$project = Project::query()
->where('public_api_key', $request->header('x-public-api-key'))
->first();

if ($project && $project->private_api_key === $request->header('x-private-api-key')) {
return $next($request);
}

return response()->json([
'error' => __('Unauthorized'),
'reason' => __('Invalid project public/private api key'),
], 401);
}
}
8 changes: 8 additions & 0 deletions application/app/Http/Requests/Project/StoreProjectRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ public function rules(): array
return [
'name' => ['required', 'string', 'min:3', 'max:128'],
'geo_blocked_countries' => ['nullable', 'string', 'min:2', 'max:128'],
'regenerate_api_keys' => ['nullable', 'in:yes'],
];
}

public function messages(): array
{
return [
'regenerate_api_keys.in' => 'This value is invalid.',
];
}
}
9 changes: 9 additions & 0 deletions application/app/Models/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;

class Project extends Model
Expand All @@ -20,4 +21,12 @@ class Project extends Model
protected $hidden = [
'private_api_key',
];

protected function privateApiKey(): Attribute
{
return Attribute::make(
get: fn (string $value) => decrypt($value),
set: fn (string $value) => encrypt($value),
);
}
}
Loading

0 comments on commit 64eb680

Please sign in to comment.