Login한 사용자에게 authorization code
를 보내주고, authorization code
를 받아 access token
을 내려준다.
즉, 사용자에 대한 validation을 수행한다.
client id, (client secret), redirect uri, grant type
flask auth server(provider)를 제공해준다.
Authlib를 사용하면 flask를 이용해 간단하게 oauth server를 구현해볼 수 있다.
📁 Authlib github 👉 example code
Resource Owner로 로그인을 하는 사용자와 관련된 객체이다.
인증을 받음을 통해 제공하는 서비스를 이용하려고 하는 주체에 해당한다.
client_id
, client_secret
, redirect_uri
, grant_type
등의 값 인증 정보를 요청할 때 필요한 값을 가지고 있는 객체로 서비스 제공자에 해당한다.
redirect_uri
, response type
,grant_type
, scope
의 값들은 client_metadata
column에 json 형태로 저장된다.
client_matadata 👇
{
"client_name": "인가 받은 client",
"client_uri": "인가 받은 client가 사용하는 주소",
"grant_types": [
"client가 사용할 수 있는 grant type들",
"authorization_code",
"password",
"refresh_token"
],
"redirect_uris": [
"callback 주소"
],
"response_types": [
"응답으로 받을 수 있는 값들"
"code",
"password",
"token"
],
"scope": "client에게 인가된 권한들",
"skip_consent": 사용자의 동의를 받을 것인지 여부(true / false),
"token_endpoint_auth_method": "token을 인증하는 method 타입(basic / post)"
}
사용자가 로그인을 시도하면 Client에서는
client id
와 (필요한 경우)client secret
을 Basic 인증 방식으로 OAuth Server로부터 인증을 받고,Client 클래스는 Authlib를 이용하면 OAuth2ClientMixin 클래스를 제공해주기 때문에, 상속 받아 활용하면 쉽게 구현할 수 있다.
from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin
# [client] - client_id, client_secret, client_metadata, expires_at
class Client(db.Model, OAuth2ClientMixin):
id = Column(Integer, primary_key=True)
user_id = Column(
Integer, ForeignKey(User.id, ondelete='CASCADE')
)
OAuth는 Bearer 방식의 token을 사용한다.
Token은 사용자에 대한 인증이 성공된 경우, 사용자가 resource를 접근하기 위해서 사용된다.
기본적으로 access token
, refresh token
, client id
, expire at
, scope
등의 정보로 구성되어있다.
Token 클래스 역시 Authlib에서 제공하는 OAuth2TokenMixin을 통해 쉽게 구현이 가능하다.
from authlib.integrations.sqla_oauth2 import OAuth2TokenMixin
# [token] - access_token, refresh_token, expires_at, scope, client_id
# 발급 받은 token에 대한 정보 저장
class Token(db.Model, OAuth2TokenMixin):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(
db.Integer, db.ForeignKey(User.id, ondelete='CASCADE')
)
user = db.relationship('User')
Authlib에는 AuthorizationServer라는 인증 요청과 응답을 처리하는 클래스를 제공해주고 있다.
간단하게 instance를 생성하고 flask app에 연결시켜주면 활용이 가능하다.
app/oauth/server.py 👇
from authlib.integrations.flask_oauth2 import AuthorizationServer
# 인증을 요청한 client 찾기
def query_client(client_id):
return db.session.query(Client).filter_by(client_id=client_id).first()
# authorization code를 받고 token을 생성하여 저장
def save_token(token_data, request):
if request.user:
user_id = request.user.get_user_id()
else:
# client_credentials grant_type
user_id = request.client.user_id
# or, depending on how you treat client_credentials
# user_id = None
token = Token(
client_id=request.client.client_id,
user_id=user_id,
**token_data
)
db.session.add(token)
db.session.commit()
oauth_server = AuthorizationServer()
추가적으로 필요한 로직이 있다면 제공되는 query_client()
, save_token()
을 수정하고, AuthorizationServer instance도 생성을 했다면 flask app을 연결시켜준다.
app/init.py 👇
from flask import Flask
app = Flask(__name__)
oauth_server.init_app(app, query_client=query_client, save_token=save_token)
config의 값을 변경하지 않아도 정상 동작하지만, 세부적인 설정을 하고 싶다면 config의 값을 변경시켜주면 된다.
OAUTH2_TOKEN_EXPIRES_IN
: 발행하는 token의 유효 기간 설정OAUTH2_TOKEN_EXPIRES_IN = {
'authorization_code': 864000,
'implicit': 3600,
'password': 864000,
'client_credentials': 864000
}
OAUTH2_ACCESS_TOKEN_GENERATOR
: access token에 관한 설정OAUTH2_REFRESH_TOKEN_GENERATOR
: refresh token 관련 설정Authorization Server에대한 설정을 마쳤으면, 인증을 받기 위한 endpoint를 작성해주면 된다.
from flask import Blueprint, request, render_template
from flask_login import current_user, login_required
from login.oauth.server import oauth_server
oauth = Blueprint('oauth', __name__)
# authorize code를 받아오는 부분 >> auth code를 받기 위해서는 login을 필요로 함
@oauth.route('/authorize', methods=['GET', 'POST'])
@login_required
def authorize():
if request.method == 'GET':
# get으로 들어오는 경우 >> 사용자로부터 consent 받기
# 사용자에게 consent를 받는 부분
try:
# grant = oauth_server.get_consent_grant(end_user=current_user)
grant = oauth_server.validate_consent_request(end_user=current_user)
client = grant.client
scope = client.get_allowed_scope(grant.request.scope)
except OAuth2Error as error:
current_app.logger.exception('oauth-error')
return error.error
# You may add a function to extract scope into a list of scopes
# with rich information, e.g.
# scopes = describe_scope(scope) # returns [{'key': 'email', 'icon': '...'}]
return render_template(
'oauth/authorize.html',
grant=grant,
user=current_user,
client=client,
scopes=scope,
)
# post로 들어오는 경우 >> 사용자의 동의 여부를 전송
# form에 동의하시겠습니까? -> 동의했는지의 여부를 서버로 보내줘야하기 때문에
# client setting >> consent를 받는지 여부에 따라서 달라지는 부분
# option : skip_content = true 이런 식으로
confirmed = request.form['confirm']
if confirmed:
# granted by resource owner
# 동의한 경우
return oauth_server.create_authorization_response(grant_user=current_user)
# 동의하지 않은 경우
return oauth_server.create_authorization_response(grant_user=None)
# auth code를 받아 token 발급
@oauth.route('/token', methods=['POST'])
def issue_token():
return oauth_server.create_token_response()
client는 접근에 대한 인가를 받기 위해 사용자를 /authorize endpoint로 보내 authorization code
를 받는다.
받은 authorization code
를 이용해 /token endpoint로 token 요청을 보내면 client는 resource에 대한 접근 권한을 인가받게 된다.