Google Cloud Endpoints에 대한 커스텀 인증 (OAuth2 대신)
Google Cloud Endpoints 에 대한 App Engine의 지원에 대해 매우 기쁘게 생각 합니다.
즉, 아직 OAuth2를 사용하지 않으며 일반적으로 사용자 이름 / 비밀번호로 사용자를 인증하므로 Google 계정이없는 고객을 지원할 수 있습니다.
무료로 얻을 수있는 모든 이점 (API 콘솔, 클라이언트 라이브러리, 견고성 등) 때문에 API를 Google Cloud Endpoints로 마이그레이션하고 싶지만 주요 질문은…
이전에 기존 API에서 유효한 사용자 세션 + CSRF 토큰을 확인한 클라우드 엔드 포인트에 사용자 지정 인증을 추가하는 방법.
protoRPC 메시지에 세션 정보 및 CSRF 토큰과 같은 항목을 추가하지 않고이를 수행 할 수있는 우아한 방법이 있습니까?
전체 애플리케이션에 webapp2 인증 시스템을 사용하고 있습니다. 그래서 저는 이것을 구글 클라우드 인증에 재사용하려고했고 그것을 얻었습니다!
webapp2_extras.auth는 webapp2_extras.sessions를 사용하여 인증 정보를 저장합니다. 그리고이 세션은 보안 쿠키, 데이터 저장소 또는 Memcache의 세 가지 형식으로 저장 될 수 있습니다.
Securecookie는 기본 형식이며 제가 사용하고 있습니다. webapp2 인증 시스템은 프로덕션 환경에서 실행되는 많은 GAE 응용 프로그램에 사용되므로 충분히 안전하다고 생각합니다.
그래서이 보안 쿠키를 디코딩하고 GAE Endpoints에서 재사용합니다. 이것이 보안 문제를 일으킬 수 있는지는 모르겠지만 (그렇지 않기를 바랍니다) 아마도 @bossylobster는 보안 측면에서 괜찮다면 말할 수 있습니다.
내 API :
import Cookie
import logging
import endpoints
import os
from google.appengine.ext import ndb
from protorpc import remote
import time
from webapp2_extras.sessions import SessionDict
from web.frankcrm_api_messages import IdContactMsg, FullContactMsg, ContactList, SimpleResponseMsg
from web.models import Contact, User
from webapp2_extras import sessions, securecookie, auth
import config
__author__ = 'Douglas S. Correa'
TOKEN_CONFIG = {
'token_max_age': 86400 * 7 * 3,
'token_new_age': 86400,
'token_cache_age': 3600,
}
SESSION_ATTRIBUTES = ['user_id', 'remember',
'token', 'token_ts', 'cache_ts']
SESSION_SECRET_KEY = '9C3155EFEEB9D9A66A22EDC16AEDA'
@endpoints.api(name='frank', version='v1',
description='FrankCRM API')
class FrankApi(remote.Service):
user = None
token = None
@classmethod
def get_user_from_cookie(cls):
serializer = securecookie.SecureCookieSerializer(SESSION_SECRET_KEY)
cookie_string = os.environ.get('HTTP_COOKIE')
cookie = Cookie.SimpleCookie()
cookie.load(cookie_string)
session = cookie['session'].value
session_name = cookie['session_name'].value
session_name_data = serializer.deserialize('session_name', session_name)
session_dict = SessionDict(cls, data=session_name_data, new=False)
if session_dict:
session_final = dict(zip(SESSION_ATTRIBUTES, session_dict.get('_user')))
_user, _token = cls.validate_token(session_final.get('user_id'), session_final.get('token'),
token_ts=session_final.get('token_ts'))
cls.user = _user
cls.token = _token
@classmethod
def user_to_dict(cls, user):
"""Returns a dictionary based on a user object.
Extra attributes to be retrieved must be set in this module's
configuration.
:param user:
User object: an instance the custom user model.
:returns:
A dictionary with user data.
"""
if not user:
return None
user_dict = dict((a, getattr(user, a)) for a in [])
user_dict['user_id'] = user.get_id()
return user_dict
@classmethod
def get_user_by_auth_token(cls, user_id, token):
"""Returns a user dict based on user_id and auth token.
:param user_id:
User id.
:param token:
Authentication token.
:returns:
A tuple ``(user_dict, token_timestamp)``. Both values can be None.
The token timestamp will be None if the user is invalid or it
is valid but the token requires renewal.
"""
user, ts = User.get_by_auth_token(user_id, token)
return cls.user_to_dict(user), ts
@classmethod
def validate_token(cls, user_id, token, token_ts=None):
"""Validates a token.
Tokens are random strings used to authenticate temporarily. They are
used to validate sessions or service requests.
:param user_id:
User id.
:param token:
Token to be checked.
:param token_ts:
Optional token timestamp used to pre-validate the token age.
:returns:
A tuple ``(user_dict, token)``.
"""
now = int(time.time())
delete = token_ts and ((now - token_ts) > TOKEN_CONFIG['token_max_age'])
create = False
if not delete:
# Try to fetch the user.
user, ts = cls.get_user_by_auth_token(user_id, token)
if user:
# Now validate the real timestamp.
delete = (now - ts) > TOKEN_CONFIG['token_max_age']
create = (now - ts) > TOKEN_CONFIG['token_new_age']
if delete or create or not user:
if delete or create:
# Delete token from db.
User.delete_auth_token(user_id, token)
if delete:
user = None
token = None
return user, token
@endpoints.method(IdContactMsg, ContactList,
path='contact/list', http_method='GET',
name='contact.list')
def list_contacts(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
model_list = Contact.query().fetch(20)
contact_list = []
for contact in model_list:
contact_list.append(contact.to_full_contact_message())
return ContactList(contact_list=contact_list)
@endpoints.method(FullContactMsg, IdContactMsg,
path='contact/add', http_method='POST',
name='contact.add')
def add_contact(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
new_contact = Contact.put_from_message(request)
logging.info(new_contact.key.id())
return IdContactMsg(id=new_contact.key.id())
@endpoints.method(FullContactMsg, IdContactMsg,
path='contact/update', http_method='POST',
name='contact.update')
def update_contact(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
new_contact = Contact.put_from_message(request)
logging.info(new_contact.key.id())
return IdContactMsg(id=new_contact.key.id())
@endpoints.method(IdContactMsg, SimpleResponseMsg,
path='contact/delete', http_method='POST',
name='contact.delete')
def delete_contact(self, request):
self.get_user_from_cookie()
if not self.user:
raise endpoints.UnauthorizedException('Invalid token.')
if request.id:
contact_to_delete_key = ndb.Key(Contact, request.id)
if contact_to_delete_key.get():
contact_to_delete_key.delete()
return SimpleResponseMsg(success=True)
return SimpleResponseMsg(success=False)
APPLICATION = endpoints.api_server([FrankApi],
restricted=False)
From my understanding Google Cloud Endpoints provides a way to implement a (RESTful?) API and to generate a mobile client library. Authentication in this case would be OAuth2. OAuth2 provides different 'flows', some of which support mobile clients. In the case of authentication using a principal and credentials (username and password) this doesn't seem like a good fit. I honestly think you would be better off by using OAuth2. Implementing a custom OAuth2 flow to support your case is an approach that could work but is very error prone. I haven't worked with OAuth2 yet but maybe an 'API key' can be created for a user so they can both use the front-end and the back-end through the use of mobile clients.
I wrote a custom python authentication library called Authtopus that may be of interest to anyone looking for a solution to this problem: https://github.com/rggibson/Authtopus
Authtopus supports basic username and password registrations and logins, as well as social logins via Facebook or Google (more social providers could probably be added without too much hassle too). User accounts are merged according to verified email addresses, so if a user first registers by username and password, then later uses a social login, and the verified email addresses of the accounts match up, then no separate User account is created.
you can used jwt for authentication. Solutions here
I did not coded it yet, but it imagined next way:
When server receives login request it look up username/password in datastore. In case user not found server responds with some error object that contains appropriate message like "User doesn't exist" or like. In case found it stored in FIFO kind of collection (cache) with limited size like 100 (or 1000 or 10000).
On successful login request server returns to client sessionid like ";LKJLK345345LKJLKJSDF53KL". Can be Base64 encoded username:password. Client stores it in Cookie named "authString" or "sessionid" (or something less eloquent) with 30 min (any) expiration.
With each request after login client sends Autorization header that it takes from cookie. Each time cookie taken, it renewed -- so it never expires while user active.
On server side we will have AuthFilter that will check presence of Authorization header in each request (exclude login, signup, reset_password). If no such header found, filter returns response to client with status code 401 (client shows login screen to user). If header found filter first checks presence of user in the cache, after in datastore and if user found -- does nothing (request handled by appropriate method), not found -- 401.
Above architecture allows to keep server stateless but still have auto disconnecting sessions.
'developer tip' 카테고리의 다른 글
Android AccountManager는 앱 / UID 단위로 OAuth 토큰을 저장하면 안 되나요? (0) | 2020.12.13 |
---|---|
Sun의 javac에서 생성 된 이상한 예외 테이블 항목 (0) | 2020.12.13 |
그렇게 오래 걸리는 Symfony 방화벽은 무엇입니까? (0) | 2020.12.13 |
div 아래 / 뒤의 콘텐츠를 흐리게 할 수 있습니까? (0) | 2020.12.13 |
UITableView의 지연로드 이미지 (0) | 2020.12.13 |