Overcoming access control in web APIs How to address security - - PowerPoint PPT Presentation

overcoming access control in web apis
SMART_READER_LITE
LIVE PREVIEW

Overcoming access control in web APIs How to address security - - PowerPoint PPT Presentation

Overcoming access control in web APIs How to address security concerns using Sanic Adam Hopkins 1 / 39 class Adam: def __init__(self): self.work = PacketFabric("Sr. Software Engineer") self.oss = Sanic("Core Maintainer")


slide-1
SLIDE 1

Overcoming access control in web APIs

How to address security concerns using Sanic

Adam Hopkins 1 / 39

slide-2
SLIDE 2

class Adam: def __init__(self): self.work = PacketFabric("Sr. Software Engineer") self.oss = Sanic("Core Maintainer") self.home = Israel("Negev") async def run(self, inputs: Union[Pretzels, Coffee]) -> None: while True: await self.work.do(inputs) await self.oss.do(inputs) def sleep(self): raise NotImplemented

PacketFabric - Network-as-a-Service platform; private access to the

cloud; secure connectivity between data centers

Sanic Framework - Python 3.6+ asyncio enabled framework and

  • server. Build fast. Run fast.

GitHub - /ahopkins T witter - @admhpkns 1 / 39

slide-3
SLIDE 3

What we will NOT cover?

TLS Password and other sensitive information storage Server security SQL injection Data validation 2 / 39

slide-4
SLIDE 4
  • 1. Authentication -
  • 2. Authorization -

no yes no yes 200 OK 401 Unauthorized 403 Forbidden

  • 1. Logged in?
  • 2. Allow access?

Do I know who this person is? Should I let them in?

3 / 39

slide-5
SLIDE 5

@app.get("/protected") async def top_secret(request): return json({"foo":"bar"})

4 / 39

slide-6
SLIDE 6

@app.get("/protected") async def top_secret(request): return json({"foo":"bar"}) curl localhost:8000/protected -i HTTP/1.1 200 OK Content-Length: 13 Content-Type: application/json Connection: keep-alive Keep-Alive: 5 {"foo":"bar"}

4 / 39

slide-7
SLIDE 7

async def do_protection(request): ... def protected(wrapped): def decorator(handler): async def decorated_function(request, *args, **kwargs): await do_protection(request) return await handler(request, *args, **kwargs) return decorated_function return decorator(wrapped) @app.get("/protected") @protected async def top_secret(request): return json({"foo": "bar"})

5 / 39

slide-8
SLIDE 8

async def do_protection(request): ... @app.middleware('request') async def global_authentication(request): await do_protection(request)

6 / 39

slide-9
SLIDE 9

Remember!

Status Code Status T ext Authentication 401 Unauthorized 🤕 Authorization 403 Forbidden ⛔ 7 / 39

slide-10
SLIDE 10

Remember!

Status Code Status T ext Authentication 401 Unauthorized 🤕 Authorization 403 Forbidden ⛔

from sanic.exceptions import Forbidden, Unauthorized async def do_protection(request): if not await is_authenticated(request): raise Unauthorized("Who are you?") if not await is_authorized(request): raise Forbidden("You are not allowed")

7 / 39

slide-11
SLIDE 11

curl localhost:8000/protected -i HTTP/1.1 401 Unauthorized Content-Length: 49 Content-Type: application/json Connection: keep-alive Keep-Alive: 5 {"error":"Unauthorized","message":"Who are you?"}

8 / 39

slide-12
SLIDE 12

async def is_authenticated(request): """How are we going to authenticate requests?"""

9 / 39

slide-13
SLIDE 13

Common authentication strategies

Basic Digest Bearer OAuth Session

10 / 39

slide-14
SLIDE 14

Common authentication strategies

Basic Digest Bearer OAuth Session

11 / 39

slide-15
SLIDE 15

Forget what you know!

12 / 39

slide-16
SLIDE 16

T rain pass 🚅

Session based

Single Ride 🎠 Point A to Point B Non-session based All day pass 🎬 Off and on at any stop 🚐 Bearer 13 / 39

slide-17
SLIDE 17

Session based 🎠

Client Server Datastore

/login using credentials /login using credentials persist session details persist session details session_id session_id /protected using session_id /protected using session_id confirm session_id confirm session_id OK OK protected resource protected resource

Client Server Datastore

aka Single Ride 🚅 14 / 39

slide-18
SLIDE 18

Non-session based 🎬

Client Server

/login using credentials /login using credentials generate token generate token token token /protected using token /protected using token confirm authenticity, etc confirm authenticity, etc protected resource protected resource

Client Server

Bearer

All day pass 🚅 15 / 39

slide-19
SLIDE 19

Hold that thought ...

16 / 39

slide-20
SLIDE 20

Let's decide on an auth strategy ...

  • 1. Who will consume the API?
  • 2. Do you have control over the client?
  • 3. Will this power a web browser frontend application?

Applications? Scripts? People?

17 / 39

slide-21
SLIDE 21

Direct API v. Browser Based API (or both)

What we really want to know is...

18 / 39

slide-22
SLIDE 22

Direct API

$ curl https://foo.bar/protected

Solved ✅

Browser Based API

fetch('https://foo.bar/protected').then(r => { console.log(response) })

Unsolved Fewer security concerns Scripts, mobile apps, non- browser clients More techinically sophisticated users API key or JWT More security concerns (CSRF, XSS) Web applications Lesser techinically sophisticated users Session ID or JWT 19 / 39

slide-23
SLIDE 23

Browser Based API Concerns

  • 1. How should the browser store the token?
  • 2. How should the browser send the token?

(XSS) Cookie, localStorage, sessionStorage, in memory (CSRF) Cookie, Authentication header

20 / 39

slide-24
SLIDE 24

T ypical recommendations

Session based 🎠

Solved ✅

Non-session based 🎬

Unsolved

Stored: Set-Cookie: token=<TOKEN> Sent: Cookie: token=<TOKEN> Subject to CSRF Fixed with: X-XSRF-TOKEN: <CSRFTOKEN> Stored: JS accessible Sent: Authorization: Bearer <TOKEN> Subject to XSS

21 / 39

slide-25
SLIDE 25

How do we authenticate?

Session based 🎠 v. Non-session based 🎬 Direct API v. Browser Based API (or both) API key v. Session ID v. JWT 22 / 39

slide-26
SLIDE 26

How do we authenticate?

Solutions:

✅ Direct API using API key in Authorization header ✅ Browser Based API using session ID in cookies Session based 🎠 v. Non-session based 🎬 Direct API v. Browser Based API (or both) API key v. Session ID v. JWT 22 / 39

slide-27
SLIDE 27

How do we authenticate?

Solutions:

✅ Direct API using API key in Authorization header ✅ Browser Based API using session ID in cookies

But what about:

Session based 🎠 v. Non-session based 🎬 Direct API v. Browser Based API (or both) API key v. Session ID v. JWT Both Direct API and Browser Based API? 22 / 39

slide-28
SLIDE 28

How do we authenticate?

Solutions:

✅ Direct API using API key in Authorization header ✅ Browser Based API using session ID in cookies

But what about:

Session based 🎠 v. Non-session based 🎬 Direct API v. Browser Based API (or both) API key v. Session ID v. JWT Both Direct API and Browser Based API? Browser Based API using non-session tokens, aka JWT s? 22 / 39

slide-29
SLIDE 29

23 / 39

slide-30
SLIDE 30

Anatomy of a JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm FtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4f wpMeJf36POk6yJV_adQssw5c 24 / 39

slide-31
SLIDE 31

Anatomy of a JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

{ "alg": "HS256", "typ": "JWT" }

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2 MjM5MDIyfQ

{ "sub": "1234567890", "name": "John Doe", "iat": 1516239022 }

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

signature

25 / 39

slide-32
SLIDE 32

Anatomy of a JWT

Set-Cookie access_token=

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm FtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ; Secure

Set-Cookie access_token_signature=

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Secure; HttpOnly 26 / 39

slide-33
SLIDE 33

Anatomy of a JWT

Set-Cookie access_token=

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibm FtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ; Secure

Set-Cookie access_token_signature=

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Secure; HttpOnly 26 / 39

slide-34
SLIDE 34

Split JWT cookies

header_payload, signature = access_token.rsplit(".", maxsplit=1) set_cookie( response, "access_token", header_payload, httponly=False ) set_cookie( response, "access_token_signature", signature, httponly=True, ) set_cookie( response, "csrf_token", generate_csrf_token(), httponly=False, ) # Do we even need this? Perhaps not! def set_cookie(response, key, value, config, httponly=None): response.cookies[key] = value response.cookies[key]["httponly"] = httponly response.cookies[key]["path"] = "/" response.cookies[key]["domain"] = "foo.bar" response.cookies[key]["expires"] = datetime(...) response.cookies[key]["secure"] = True

27 / 39

slide-35
SLIDE 35

We found a winner 🏇

Stateless JWT based 🎬

Solved ✅

Non-session

Stored: 2 cookies JS accessible Sent: 2 cookies Also, 1 token via Header for CSRF protection

Authorization: Bearer <TOKEN>

Secured from XSS Subject to

28 / 39

slide-36
SLIDE 36

def extract_token(request): access_token = request.cookies.get("access_token") access_token_signature = request.cookies.get("access_token_signature") return f"{access_token}.{access_token_signature}" def is_authenticated(request): token = extract_token(request) try: jwt.decode(token, ...) except Exception: return False else: return True

29 / 39

slide-37
SLIDE 37

def do_protection(request): if not is_authenticated(request): raise Unauthorized("Who are you?") if not is_authorized(request): raise Forbidden("You are not allowed") if not is_pass_csrf(request): raise Forbidden("You CSRF thief!")

30 / 39

slide-38
SLIDE 38

def is_authorized(request): """How shall we do this?"""

31 / 39

slide-39
SLIDE 39

Structured Scopes

user:read:write

namespace:action(s) 32 / 39

slide-40
SLIDE 40

Structured Scopes

user:read:write

namespace:action(s)

user:read

32 / 39

slide-41
SLIDE 41

Structured Scopes

user:read:write

namespace:action(s)

user:read Pass ✅

32 / 39

slide-42
SLIDE 42

from sscopes import validate is_valid = validate("user:read:write", "user:read") print(is_valid) # True

33 / 39

slide-43
SLIDE 43

def is_authorized(request, base_scope): if base_scope: token = extract_token(request) payload = token.decode(token, ...) return validate(base_scope, payload.get("scopes")) return True

34 / 39

slide-44
SLIDE 44

@app.get("/protected") @protected("user:read") async def top_secret(request): return json({"foo":"bar"})

35 / 39

slide-45
SLIDE 45

@app.get("/protected") @protected("user:read") async def top_secret(request): return json({"foo":"bar"}) fetch('https://foo.bar/protected').then(async response => { console.log(await response.json()) })

35 / 39

slide-46
SLIDE 46

There must be a better way 🤰

36 / 39

slide-47
SLIDE 47

There must be a better way 🤰

pip install sanic-jwt

36 / 39

slide-48
SLIDE 48

from sanic_jwt import Initialize, decorators async def authenticate(request): """Check that username and password are valid""" async def retrieve_user(request): """Get a user object from DB storage""" async def my_scope_extender(user): return user.scopes app = Sanic() Initialize( app, authenticate=authenticate, # sanic-jwt required handler retrieve_user=retrieve_user, add_scopes_to_payload=my_scope_extender, cookie_set=True, # Set and accept JWTs in cookies cookie_split=True, # Expect split JWT cookies cookie_strict=False, # Allow fallback to Authorization header ) @app.get("/protected") @decorators.scoped("user:read") async def top_secret(request): ...

37 / 39

slide-49
SLIDE 49

https://foo.bar/auth # Login with username/password https://foo.bar/auth/verify # Verify a valid JWT was passed https://foo.bar/auth/me # View details of current user https://foo.bar/protected # Must have user:read access

38 / 39

slide-50
SLIDE 50

Presentation Repo - /ahopkins/europython2020-overcoming-access-control PacketFabric - https://packetfabric.com Sanic Repo - /huge-success/sanic Sanic Community - Forums sanic-jwt - /ahopkins/sanic-jwt sscopes - Docs 39 / 39