Master Application Security

Ever thought How does a website remembers you? Why do we hash password? if simple password login was working, then what was the need of OAuth or OIDC (OpenID Connect)? Why roles exists?
Today, we are going to find out the answers to such questions, we'll cover the one of the most important feature of an application and that is Security!
Why Application Security Matters
Security is something that isn't there in your application with magic, it's something you have to build, and there are multiple reasons for it.
Common security risks in web applications
Access Controls: Users may use some feature that was meant to be admin-only.
Injection Attacks: User input may contain DB Queries or Code that can leak data.
Authentication: A hacker may be able to get sessions or token of your user and access their accounts.
Cryptographic Failures: Sensitive data sent over HTTP protocol without encryption may get leaked.
Logging: If system security events aren't logged, someone may steal all of your data, without you ever got to know that you are under attack.
These are a few common security risks, there exists a lot of more than that.
Why Authentication and Authorization are different problems
A lot of people get confused in Authentication and Authorization, they think they are the same thing, but they actually aren't. Lemme be clear about it.
Authentication means "Do I know you". You store who user is and the details related to him/her here.
Authorization means "Are you allowed to do this". You tell what are the things user is allowed to do, what are the endpoints that user can access.
The cost of insecure systems
Insecure systems, when breached, face a major financial loss followed by legal cases, damage to brand reputation and users trust may fade away. That is why big companies spend millions of dollars just to make their system secure and durable.
Real-world examples of security failures
Trezor / ShipMonk Breach: Hardware wallet supplier Trezor, had its operations managed by ShipMonk which experienced a data leak of names, phone numbers, and addresses of nearly 14,000 customers across 7 countries.
Tata Electronics: The electronic manufacturing giant fell victim to an extortion plot involving hackers who leaked hundreds of GB of data about their supply chain and critical clients, including Tesla and Apple.
Instructure / Canvas LMS: This learning platform provider's Free-For-Teacher system was infiltrated by ShinyHunters hacking group, which stole nearly 3.65 TB of data from the education technology company Instructure.
Password Hasing and Storage
Coming back to one of our starting questions, Why do we even need to store password by hasing them first.
Why passwords should never be stored in plain text
Imagine you're building an app, and you just store the password exactly as the user typed it, Feels harmless right? but...
users table (never do this)
────────────────────────────────
id email password
1 sarah@example.com mypassword123
2 raj@example.com football2020
If your DB breached, all of your users password will be leaked and since most people use the same password on multiple sites, the attacker doesn't just get access to your app, they may get access to that user's email, banking apps, etc.
One of the biggest real examples of this is LinkedIn, back in 2012, they got hacked and nearly 6.5 million passwords were stolen. The actual number was much bigger, around 117 million passwords were compromised.
What hashing is
Hashing is basically running your password through a one-way function, which converts it into a fixed length string of random-looking characters.
"mypassword123" ──▶ hash function ──▶ "$2b$10$N9qo8uLOickgx2ZMRZoMy..."
Key word here is one-way. You can hash a password, but you can't reverse a hash back to get the original password. So even if the hash leaks, the actual password stays hidden.
How bcrypt works
bcrypt is one of the most commonly used hashing algorithms for passwords specifically, and there's a reason for that.
It's slow on purpose: If hashing took a fraction of a second, an attacker could try millions of password guesses per second. bcrypt is built to be slow, so brute forcing becomes way too expensive.
It has a work factor (cost): You can tell bcrypt how slow to be, so as computers get faster over the years, you can just crank up this number to keep it hard to crack.
Salt and hashing concepts
If two users use the same password, and you hash it directly, there hash will look exactly same. That's a problem, because attackers keep giant precomputed tables of common password hashes (rainbow tables), and if your hash matches one of those, boom, password cracked instantly.
That's why LinkedIn's breach was so bad, their passwords were hashed but not salted, which made them relatively easy to crack, and as this data resurfaced later, 90% of the passwords were cracked within just 72 hours.
A salt fixes this. It's a random string of characters added to the password before hashing, so even same passwords look completely different.
"password123" + salt "x8f2" ──▶ hash A
"password123" + salt "q1z9" ──▶ hash B (same password, totally different hash)
The good part, bcrypt handles the salting for you automatically, you don't have to write that logic yourself.
Password verification process
Now here's a question, If hashing can't be reversed, how does login work?
Because... It just hashes whatever the user typed again, and compares that new hash with the one stored in your DB.
Password Hashing Workflow
Signup:
User enters password ──▶ bcrypt hashes it (adds a salt automatically) ──▶ Store the hash
Login:
User enters password ──▶ bcrypt hashes it (same salt used before) ──▶ Compare with stored hash
│
┌───────────┴───────────┐
▼ ▼
Matches Doesn't match
▼ ▼
Login allowed Login rejected
Best practices for password storage
Never store plain text passwords, not even during development
Use a proper password-hashing algorithm like bcrypt, not a plain fast hash like MD5 or SHA-1
Let bcrypt handle the salting, don't try building your own logic for it
Set a reasonable minimum password length at signup
Authentication vs Authorization
We already discussed it briefly earlier, but let's now gather a more of clarity.
What authentication means
Authentication: the app confirming, "yes, I know who you are." When you log in with your email and password, and it's correct, the app knows specifically who you are, not some random person.
What authorization means
Authorization: Once the app knows who you are, it still has to check, are you allowed to do this specific thing? Can you view this page? Can you delete this post? Can you access the admin dashboard?
Why applications need both
An app can authenticate you flawlessly, know exactly who you are, and still need to check authorization separately for literally every action you try to take. Knowing your identity doesn't automatically tell the system what you're allowed to touch.
| Authentication | Authorization | |
|---|---|---|
| Question | "Do I know you?" | "Are you allowed to do this?" |
| When it happens | Once, at login | Checked on every protected action |
| Verified using | Password, token, etc. | Roles/permissions |
| Result | Identity confirmed | Action allowed or denied |
Examples from everyday applications
Think about a Google Doc someone shared with you. Logging into your Google account, that's authentication, Google now knows who you are. But whether you can only view that doc, or actually edit it too, depends on what permission the owner gave you, that part is authorization.
Role-Based Access Control (RBAC)
Once your app has more than just "logged in" or "not logged in", you need a way to manage who can do what. That's where RBAC comes into the picture.
What RBAC is
Role-Based Access Control means, instead of assigning permissions to every single user one by one, you assign permissions to roles, and just assign users to a role. Users automatically get whatever their role has.
| User | Role | Permissions |
|---|---|---|
| Sarah | Admin | Create, Edit, Delete, Manage Users |
| Raj | Moderator | Edit, Delete |
| Alex | User | View Content |
Designing role hierarchies
A lot of apps design roles so that a higher role includes everything a lower role can do, plus a bit extra.
RBAC Role Hierarchy
Admin
└── everything Moderator can do, plus: manage users, change settings
Moderator
└── everything User can do, plus: edit and delete content
User
└── view content, create own posts, edit own profile
Common RBAC patterns
Flat roles: A small, fixed list, like
adminanduser. Works fine for smaller apps.Hierarchical roles: Roles build on top of each other, like the example above.
Permission-based roles: Instead of hardcoding what each role can do, you define individual permissions (
can_edit_post,can_delete_user), and a role is just a bundle of these.
Admin vs User vs Moderator examples
On a content platform:
Admin manages users, edit site settings, and can moderate everything
Moderator can edit or remove content, but has no access to user accounts or settings
User can create their own content, and only edit or delete what they personally made
Scaling permissions in large applications
As your app grows, sprinkling if role === "admin" checks all across your codebase gets messy fast, and it's easy to miss a check somewhere. That's why bigger apps usually move to permission-based checks instead, something like if user.can("delete_post"), so all the permission logic lives in one place. Adding a new role later, like Support, gets a lot easier this way.
OAuth 2.0 Explained
Now let's answer one of the questions we started with, if login with email and password already works, why do we even need OAuth?
What problem OAuth solves
If a third-party app wants to access your Google Contacts, to help you find friends who already use it. A terrible, solution would be, you just give your Google username and password directly. OAuth exists to solve this.
Why sharing passwords is dangerous
If you give a random third-party app your real Google password:
That app now has full access to your entire Google account, not just contacts
You have no way to limit what it can or can't do
You can't revoke its access without resetting your password everywhere you use it
If that app ever gets hacked, your actual Google password goes with it
Authorization flow overview
OAuth fixes this by making sure the third-party app never sees your password at all. You log in directly on Google's own page, and Google hands the app a limited, revokable token instead.
OAuth Authorization Flow
You ──▶ Click "Login with Google" on Third-Party App
You ──▶ Get redirected to Google's actual login page
You ──▶ Log in directly on Google (the app never sees this)
You ──▶ Google asks: "Allow ThirdPartyApp to access your contacts?"
You ──▶ You approve
Google ──▶ Sends an Access Token back to the Third-Party App
Third-Party App ──▶ Uses that token to only fetch your contacts, nothing more
Resource owner
That's you. You own the data (contacts, profile), and you decide who gets to access it.
Client application
This is the third-party app asking for access. It never see your actual Google password.
Authorization server
This is Google's own server, the one handling your real login, and deciding whether to issue a token to an app based on your approval.
Access tokens
An access token is a short-lived piece of data that basically says, "this app is allowed to access these specific things, for this specific user." Not your password, scoped down to limited permissions, and it can expire or get revoked whenever needed.
Real-world examples such as "Login with Google"
Anytime you see "Continue with Google," "Sign in with GitHub," or "Login with Facebook," that's OAuth doing its job. You log in with the actual provider directly, and that provider hands the app a limited token instead of your real credentials.
OpenID Connect (OIDC)
OAuth by itself was never really built for authentication, it was built for authorization, letting an app access your data. So then how does "Login with Google" actually log you in? That's where OIDC comes into the picture.
What OIDC is
OpenID Connect is basically a layer built on top of OAuth 2.0, adding a proper, standard way to verify who a user actually is, not just letting an app access their data.
How it extends OAuth
Plain OAuth gives you an access token, useful for accessing an API on someone's behalf. OIDC adds one more piece on top, an ID token, which tells the app exactly who just logged in.
OAuth alone: "Here's a token to access this user's contacts."
OAuth + OIDC: "Here's a token to access this user's contacts, AND here's proof this is definitely sarah@example.com."
Authentication vs Authorization
The access token (from OAuth) handles authorization, what the app can access. The ID token (from OIDC) handles authentication, confirming exactly who the person is.
ID tokens
An ID token usually carries basic identity info, the user's email, name, and a unique user ID, all digitally signed by the authorization server, so the app can trust it wasn't tampered with somewhere along the way.
User identity verification
OpenID Connect Identity Flow
You ──▶ Log in with Google via OIDC
Google ──▶ Sends back:
Access Token (for accessing your data, if needed)
ID Token (proof of who you actually are)
App ──▶ Reads the ID Token ──▶ Confirms: "this is definitely sarah@example.com"
App ──▶ Creates a session for Sarah, logs her in
Modern login systems
This is exactly what's behind every "social login" button you use today. When you click "Sign in with Google" and the app actually logs you in (not just accesses part of your data), it's OIDC riding on top of OAuth confirming your identity.
Modern Authentication Architecture
Traditional login systems
The classic setup, user signs up with email and password, the app hashes and stores the password, and login just checks that hash matches. Still very common, especially when apps want full control over their own users.
Social login systems
Instead of managing passwords at all, the app relies on OAuth and OIDC, letting users log in through Google, GitHub, or whatever provider.
Enterprise authentication
Bigger companies usually have employees logging into a bunch of internal tools, and nobody wants a separate account for each one. This gets handled through a centralized identity provider (like Okta or Azure AD), which every internal app just trusts, instead of every app managing its own separate user list.
Single Sign-On (SSO)
SSO lets a user log in once, and get access to multiple connected apps without logging in again for each one.
Single Sign-On Architecture
User logs in once, at the Identity Provider
│
▼
Identity Provider issues a session/token
│
┌────┼────┬────────────┐
▼ ▼ ▼ ▼
App A App B App C App D
(all trust the same Identity Provider,
no separate login needed for each one)
Authentication in modern SaaS products
Most modern SaaS products end up combining several of these together, traditional email/password login, social login through OAuth and OIDC, and sometimes SSO for enterprise customers, all feeding into the same underlying user and permission system, the RBAC layer we already covered.
Modern Authentication System Architecture
Email/Password Login Social Login (OAuth/OIDC) Enterprise SSO
│ │ │
└────────────────────────┼─────────────────────────┘
▼
Unified User Identity
│
▼
RBAC (Roles & Permissions)
│
▼
Access to App Features
Security Best Practices
Strong password policies
Require a decent minimum length, and don't go overboard with weird symbol rules, Length matters way more than a random mix of symbols.
Multi-factor authentication
MFA adds a second layer on top of just your password, usually a code from an authenticator app, or an OTP over text. Even if your password leaks, an attacker still can't log in without that second factor.
Login with MFA
Password correct ──▶ Prompt for second factor (OTP, app approval) ──▶ Access granted
Token expiration
Access tokens and sessions shouldn't live forever. Keeping them short-lived limits the damage if a token ever does get stolen, since it naturally stops working after some time.
Access Token Lifecycle
Issued ──▶ Valid for a limited time ──▶ Expires ──▶ Needs a refresh or a fresh login
Secure storage of credentials
Passwords get hashed, But tokens and secrets should never end up hardcoded directly in your source code, and definitely shouldn't get pushed into a Git repo. Use proper secret management for that.
Principle of least privilege
Every user, role, and even system component should only get the bare minimum access it needs to do its job. This way, if something goes wrong, the blast radius stays as small as possible.
Protecting sensitive routes
Every protected action needs to check both authentication and authorization, and that check has to happen on the server, every single time, not just by hiding a button somewhere in the UI.
Secure Application Security Layers
Password Hashing (bcrypt + salt)
│
Authentication (do I know you?)
│
Authorization / RBAC (are you allowed to do this?)
│
Token-based Access (OAuth / OIDC, tokens that expire)
│
MFA (extra proof, beyond just a password)
│
Least Privilege (minimum access, everywhere, always)
Quick Recap
Passwords should always be hashed with something like bcrypt, and salted, never stored as plain text, LinkedIn's 2012 breach is proof enough of what happens when you skip this.
Authentication answers "do I know you?" Authorization answers "are you allowed to do this?" Two different checks, handled separately, every time.
RBAC assigns permissions to roles instead of individual users, keeping things manageable as your app grows.
OAuth lets a third-party app access your data without ever seeing your password, using a scoped, revokable access token instead.
OIDC builds on top of OAuth to actually verify identity, using an ID token, this is exactly what powers "Login with Google" style buttons.
SSO lets a user log in once and get into multiple trusted apps, without logging in again and again for each one.
Security best practices, MFA, token expiration, least privilege, checking authorization on every protected route, none of these work alone, they work together, as layers.
Every layer here exists because the layer above it, on its own, just isn't enough. That's really what application security comes down to, not one clever trick, but a bunch of simple, well understood layers, stacked on top of each other.






