惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
Stack Overflow Blog
Stack Overflow Blog
L
LINUX DO - 最新话题
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
Spread Privacy
Spread Privacy
www.infosecurity-magazine.com
www.infosecurity-magazine.com
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
Microsoft Azure Blog
Microsoft Azure Blog
IT之家
IT之家
V
Vulnerabilities – Threatpost
K
Kaspersky official blog
S
Schneier on Security
B
Blog
The Register - Security
The Register - Security
SecWiki News
SecWiki News
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security Affairs
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
T
Tenable Blog
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
D
DataBreaches.Net
S
Secure Thoughts
Security Latest
Security Latest
H
Heimdal Security Blog
The Hacker News
The Hacker News
O
OpenAI News
AWS News Blog
AWS News Blog
量子位
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
腾讯CDC
U
Unit 42
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
T
The Exploit Database - CXSecurity.com
NISL@THU
NISL@THU
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hugging Face - Blog
Hugging Face - Blog
The Last Watchdog
The Last Watchdog
Recorded Future
Recorded Future
V2EX - 技术
V2EX - 技术
爱范儿
爱范儿
F
Full Disclosure

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
[Lime #1] OAuth Login
MinBapE · 2026-05-09 · via DEV Community

Today's Work

I decided on and implemented the login flow for Lime v1.

At first, I considered supporting both OAuth login and email/password sign-up. However, building email sign-up properly would bring in many additional features, such as password hashing, email verification, and password recovery.

At this stage, I wanted to move quickly and focus on the core product. So for v1, I decided to support only Google and Discord OAuth.

In Lime v1, users do not sign up with an email and password.

They log in with a Google or Discord account, and the backend automatically creates Lime's User.

After a successful login, the backend issues a JWT Access Token and a Refresh Token for our service.


What I Built

  • Google OAuth login
  • Discord OAuth login
  • OAuth callback handling
  • Automatic Lime User creation on first login
  • Linking OAuth accounts to existing users
  • JWT Access Token issuance
  • Refresh Token issuance and storage
  • Refresh Token rotation
  • Logout
  • Handling OAuth failure and cancellation cases

Overall Flow

At first, I thought OAuth was simply about adding a "social login" button.

But the actual flow was longer than I expected.

Frontend
  -> Backend: /auth/google/start
  -> Google OAuth Page
  -> Backend: /auth/google/callback?code=...&state=...
  -> Google UserInfo API
  -> Create or find Lime User
  -> Issue JWT Access Token + Refresh Token

Enter fullscreen mode Exit fullscreen mode

I designed the APIs like this.

GET  /auth/{provider}/start
GET  /auth/{provider}/callback
POST /auth/refresh
POST /auth/signout

Enter fullscreen mode Exit fullscreen mode

Currently, there are two supported providers.

google
discord

Enter fullscreen mode Exit fullscreen mode


Splitting It with VSA

This project is built with Vertical Slice Architecture, not around MVC Controllers.

So I did not put all authentication logic into one large controller. Instead, I split it by feature flow.

Features/Auth
  StartOAuthLogin
  HandleOAuthCallback
  RefreshSession
  SignOut
  OAuth
  Users
  Sessions
  Cookies

Enter fullscreen mode Exit fullscreen mode

At first, having more files made it look more complicated.

But OAuth mixes several responsibilities.

  • Redirecting to the provider
  • Handling the callback
  • Finding or creating a user
  • Issuing tokens
  • Writing cookies
  • Handling failure cases

Putting all of this into one file would be faster at first, but it would probably become harder to read later.

So this time, I chose to split the code by flow.


Provider Abstraction

Google and Discord have similar OAuth flows, but they use different URLs and return different userinfo response formats.

So I created a common interface.

internal interface IOAuthProvider
{
    string Name { get; }
    string BuildAuthorizeUrl(string state, string redirectUri);
    Task<OAuthUserInfo> ExchangeAndFetchAsync(
        string code,
        string redirectUri,
        CancellationToken ct);
}

Enter fullscreen mode Exit fullscreen mode

Each provider has its own implementation.

GoogleOAuthProvider
DiscordOAuthProvider

Enter fullscreen mode Exit fullscreen mode

The callback endpoint finds and uses the correct provider based on the provider name.

If I add Apple login later, I can extend this by adding an AppleOAuthProvider and registering it in DI.


The Most Confusing Part: Cookies and State

The most confusing part of this work was cookies.

Cookies are used by the browser, so why am I creating cookies in backend OAuth code?

At first, I could not fully understand this.

But OAuth is a flow that goes through browser redirects.

Because of that, the backend needs a way to temporarily remember values between requests.

A typical example is state.

Where state Is Stored

When OAuth starts, the backend creates a random state value.

Then it stores this value in two places.

1. The query string of the provider authorize URL
2. A browser cookie

Enter fullscreen mode Exit fullscreen mode

When the provider redirects back to the callback endpoint, it includes the state value in the query string.

The backend compares the state from the callback with the state stored in the cookie.

Same      -> Valid OAuth flow
Different -> Reject

Enter fullscreen mode Exit fullscreen mode

This is used to prevent CSRF attacks.

In this implementation, I created temporary cookies for the OAuth flow.

lime_oauth_state   -> State value for CSRF protection
lime_oauth_return  -> Path to return to after successful login

Enter fullscreen mode Exit fullscreen mode

Since both values do not need to live for a long time, I made them short-lived cookies and deleted them after the callback was handled.


returnTo Should Not Be Trusted As-Is

To send users back to the original page after a successful login, I accepted a returnTo value.

However, this value should not be trusted as-is.

For example, redirecting directly to values like these can be dangerous.

https://evil.com
//evil.com
\evil

Enter fullscreen mode Exit fullscreen mode

So I did not allow external URLs and only allowed internal paths.

/dashboard
/profile

Enter fullscreen mode Exit fullscreen mode

It is a small detail, but for a login feature, these details matter.


Linking Users and OAuth Accounts

Lime has its own User, and OAuth provider account information is stored in a separate table.

users
user_oauth_accounts
refresh_tokens

Enter fullscreen mode Exit fullscreen mode

In OAuth login, the most reliable identifier is not the email address.

It is this combination.

provider + providerUserId

Enter fullscreen mode Exit fullscreen mode

An email address can change, and whether it is verified depends on the provider.

So during login, I first look for an existing OAuth account using provider + providerUserId.

OAuth account already linked
  -> Log in as that User

OAuth account not linked
  -> If the email is verified, link it to an existing User
  -> Otherwise, create a new User

Enter fullscreen mode Exit fullscreen mode

At first, it is easy to think, "If the email is the same, isn't it the same user?"

But in authentication, whether the email is verified matters.


JWT and Refresh Tokens

I did not use the OAuth provider's access token directly for Lime API authentication.

Google or Discord access tokens are meant for calling the provider's APIs.

The token used to call Lime APIs should be issued by our own service.

After a successful login, I issue two tokens.

Token Purpose
Access Token Authenticate Lime API requests
Refresh Token Reissue Access Tokens

The Access Token is a JWT.

It contains minimal claims such as the user ID, email, and name, and it has a short lifetime.

The Refresh Token is a random string and is stored in the database.

However, I do not store the raw token. I only store its SHA-256 hash.

When a Refresh Token is used, the existing token is revoked and a new one is issued.

This is called refresh token rotation.

Keep the Access Token short-lived and the Refresh Token longer-lived.

But make sure the Refresh Token can be controlled through the database.

That was the basic direction for this implementation.


Storing Tokens in Cookies

In this implementation, both the Access Token and Refresh Token are stored in HttpOnly cookies.

Storing tokens in localStorage is also possible, but considering XSS, HttpOnly cookies can be a safer choice.

HttpOnly cookies cannot be read by JavaScript.

However, when using cookies, CSRF also needs to be considered.

So I explicitly configured options such as SameSite, Secure, and Path.

Also, ASP.NET Core's JWT Bearer authentication looks for the token in the Authorization header by default.

Since I store the access token in a cookie, I configured the authentication middleware to read the token from the cookie.

If this part is missed, tokens may be issued successfully, but APIs that require authentication will still treat the request as unauthenticated.


Response Format

I also defined a response format to align with the frontend.

Instead of returning a human-readable message in failure responses, I decided to return a stable code.

{
  "code": "INVALID_REFRESH_TOKEN",
  "data": null
}

Enter fullscreen mode Exit fullscreen mode

The frontend can use this code to decide what message to show to the user.

This keeps localization and UI wording changes separate from the backend.

I also handled the OAuth cancellation case separately.

OAUTH_CANCELLED

Enter fullscreen mode Exit fullscreen mode

When a user cancels on the provider's authentication page, it is closer to a normal failure flow than a system error.

So I separated it from ordinary missing-parameter cases.


What I Learned About Configuration

I also ran into an issue while binding configuration with IOptions<AuthOptions>.

The path the code expected was different from the actual structure in appsettings.json.

Expected by code: Auth:Jwt:SigningKey
Actual setting:   Jwt:SigningKey

Enter fullscreen mode Exit fullscreen mode

Even when a value is empty, it is not always obvious at first.

This is especially important for Jwt:SigningKey, because if it is empty, the backend cannot create or validate JWTs.

One thing I learned from this is that configuration values are just as important as code.

OAuth Client ID/Secret, JWT SigningKey, and database connection settings live outside the code, but they are core parts of making the feature actually work.


Summary

With this work, the authentication foundation for Lime v1 is now in place.

What I built was not just a "Google login button."

In practice, all of these pieces had to work together.

  • OAuth redirect flow
  • state for CSRF protection
  • Temporary OAuth cookies
  • Safe returnTo handling
  • Fetching user information from the provider
  • Automatic Lime User creation
  • Linking OAuth accounts
  • JWT Access Token issuance
  • Refresh Token storage and rotation
  • Logout
  • Response codes aligned with the frontend

OAuth requires more surrounding design than I expected.

The successful login path may look simple, but for a real service, user mapping, token storage, cookie security, and failure cases all need to be considered together.

Next, I plan to apply this authentication middleware to protected APIs and align the frontend behavior around these response codes.