Skip to content
GitLab
Explore
Sign in
Register
Primary navigation
Search or go to…
Project
C
core
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Wiki
Requirements
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Snippets
Locked files
Build
Pipelines
Jobs
Pipeline schedules
Test cases
Artifacts
Deploy
Releases
Package registry
Container Registry
Model registry
Operate
Environments
Terraform modules
Monitor
Incidents
Service Desk
Analyze
Value stream analytics
Contributor analytics
CI/CD analytics
Repository analytics
Code review analytics
Issue analytics
Insights
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to JiHu GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
Chenhao Ma
core
Commits
f4aad485
Commit
f4aad485
authored
8 years ago
by
Megan Henning
Browse files
Options
Downloads
Patches
Plain Diff
Rework oauth flow
parent
c4f3f15a
No related branches found
Branches containing commit
No related tags found
Tags containing commit
No related merge requests found
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
api/auth/authproviders.py
+114
-0
114 additions, 0 deletions
api/auth/authproviders.py
api/web/base.py
+107
-93
107 additions, 93 deletions
api/web/base.py
with
221 additions
and
93 deletions
api/auth/authproviders.py
0 → 100644
+
114
−
0
View file @
f4aad485
import
requests
from
.
import
APIAuthProviderException
,
APIUnknownUserException
from
..
import
config
,
util
log
=
config
.
log
AuthProviders
=
util
.
Enum
(
'
AuthProviders
'
,
{
'
google
'
:
GoogleAuthProvider
,
'
ldap
'
:
JWTAuthProvider
,
'
wechat
'
:
WechatAuthProvider
})
class
AuthProvider
(
object
):
"""
This class provides access to mongodb collection elements (called containers).
It is used by ContainerHandler istances for get, create, update and delete operations on containers.
Examples: projects, sessions, acquisitions and collections
"""
def
__init__
(
self
,
auth_type
):
try
:
self
.
config
=
config
.
get_auth
(
auth_type
)
except
KeyError
:
raise
NotImplementedError
(
'
Auth type {} is not supported by this instance
'
.
format
(
auth_type
))
@staticmethod
def
factory
(
auth_type
):
"""
Factory method to aid in the creation of an AuthProvider instance
when auth_type is dynamic.
"""
if
auth_type
in
AuthProviders
:
provider_class
=
AuthProviders
[
auth_type
].
value
return
provider_class
()
except
:
raise
NotImplementedError
(
'
Auth type {} is not supported
'
.
format
(
auth_type
))
class
JWTAuthProvider
(
AuthProvider
):
def
__init__
(
self
):
super
(
JWTAuthProvider
,
self
).
__init__
(
AuthProviders
.
ldap
.
key
)
def
validate_code
(
code
):
uid
=
self
.
validate_user_exists
(
code
)
return
code
,
None
,
uid
def
validate_user_exists
(
token
):
r
=
requests
.
post
(
self
.
config
[
'
id_endpoint
'
],
data
=
{
'
token
'
:
token
})
if
not
r
.
ok
:
raise
APIAuthProviderException
(
'
User token not valid
'
)
uid
=
json
.
loads
(
r
.
content
).
get
(
'
mail
'
)
if
not
uid
:
raise
APIAuthProviderException
(
'
Auth provider did not provide user email
'
)
return
uid
class
GoogleOAuthProvider
(
AuthProvider
):
def
__init__
(
self
):
super
(
GoogleAuthProvider
,
self
).
__init__
(
AuthProviders
.
google
.
key
)
def
validate_code
(
code
):
payload
=
{
'
client_id
'
:
self
.
config
[
'
client_id
'
]
'
client_secret
'
:
self
.
config
[
'
client_secret
'
]
'
code
'
:
code
,
'
grant_type
'
:
'
authorization_code
'
}
r
=
requests
.
post
(
self
.
config
[
'
token_url
'
],
data
=
payload
)
if
not
r
.
ok
:
raise
APIAuthProviderException
(
'
User code not valid
'
)
response
=
json
.
loads
(
r
.
content
)
token
=
response
[
'
access_token
'
]
refresh_token
=
response
[
'
refresh_token
'
]
uid
=
self
.
validate_user_exists
(
token
)
return
token
,
refresh_token
,
uid
def
validate_user_exists
(
token
):
r
=
requests
.
get
(
self
.
config
[
'
id_endpoint
'
],
headers
=
{
'
Authorization
'
:
'
Bearer
'
+
token
})
if
not
r
.
ok
:
raise
APIAuthProviderException
(
'
User token not valid
'
)
uid
=
json
.
loads
(
r
.
content
).
get
(
'
email
'
)
if
not
uid
:
raise
APIAuthProviderException
(
'
Auth provider did not provide user email
'
)
return
uid
class
WechatOAuthProvider
(
AuthProvider
):
def
__init__
(
self
):
super
(
WechatAuthProvider
,
self
).
__init__
(
AuthProviders
.
wechat
.
key
)
def
validate_code
(
code
):
payload
=
{
'
client_id
'
:
self
.
config
[
'
client_id
'
]
'
client_secret
'
:
self
.
config
[
'
client_secret
'
]
'
code
'
:
code
,
'
grant_type
'
:
'
authorization_code
'
}
r
=
requests
.
post
(
self
.
config
[
'
token_url
'
],
data
=
payload
)
if
not
r
.
ok
:
raise
APIAuthProviderException
(
'
User code not valid
'
)
response
=
json
.
loads
(
r
.
content
)
token
=
response
[
'
access_token
'
]
refresh_token
=
response
[
'
refresh_token
'
]
uid
=
response
[
'
openid
'
]
return
token
,
refresh_token
,
uid
This diff is collapsed.
Click to expand it.
api/web/base.py
+
107
−
93
View file @
f4aad485
import
base64
import
datetime
import
json
import
jsonschema
import
os
import
pymongo
import
requests
import
traceback
...
...
@@ -13,6 +15,7 @@ from .. import files
from
..
import
config
from
..types
import
Origin
from
..
import
validators
from
..auth.authproviders
import
AuthProvider
from
..dao
import
APIConsistencyException
,
APIConflictException
,
APINotFoundException
,
APIPermissionException
,
APIValidationException
,
dbutil
from
..dao.hierarchy
import
get_parent_tree
from
..web.request
import
log_access
,
AccessType
...
...
@@ -31,7 +34,7 @@ class RequestHandler(webapp2.RequestHandler):
drone_request
=
False
user_agent
=
self
.
request
.
headers
.
get
(
'
User-Agent
'
,
''
)
access
_token
=
self
.
request
.
headers
.
get
(
'
Authorization
'
,
None
)
session
_token
=
self
.
request
.
headers
.
get
(
'
Authorization
'
,
None
)
drone_secret
=
self
.
request
.
headers
.
get
(
'
X-SciTran-Auth
'
,
None
)
drone_method
=
self
.
request
.
headers
.
get
(
'
X-SciTran-Method
'
,
None
)
drone_name
=
self
.
request
.
headers
.
get
(
'
X-SciTran-Name
'
,
None
)
...
...
@@ -40,18 +43,18 @@ class RequestHandler(webapp2.RequestHandler):
if
site_id
is
None
:
self
.
abort
(
503
,
'
Database not initialized
'
)
if
access
_token
:
if
access
_token
.
startswith
(
'
scitran-user
'
):
if
session
_token
:
if
session
_token
.
startswith
(
'
scitran-user
'
):
# User (API key) authentication
key
=
access
_token
.
split
()[
1
]
key
=
session
_token
.
split
()[
1
]
self
.
uid
=
self
.
authenticate_user_api_key
(
key
)
elif
access
_token
.
startswith
(
'
scitran-drone
'
):
elif
session
_token
.
startswith
(
'
scitran-drone
'
):
# Drone (API key) authentication
# When supported, remove custom headers and shared secret
self
.
abort
(
401
,
'
Drone API keys are not yet supported
'
)
else
:
# User (oAuth) authentication
self
.
uid
=
self
.
authenticate_user_token
(
access
_token
)
self
.
uid
=
self
.
authenticate_user_token
(
session
_token
)
# Drone shared secret authentication
elif
drone_secret
is
not
None
:
...
...
@@ -118,7 +121,7 @@ class RequestHandler(webapp2.RequestHandler):
self
.
abort
(
401
,
'
Invalid scitran-user API key
'
)
def
authenticate_user_token
(
self
,
access
_token
):
def
authenticate_user_token
(
self
,
session
_token
):
"""
AuthN for user accounts. Calls self.abort on failure.
...
...
@@ -127,111 +130,121 @@ class RequestHandler(webapp2.RequestHandler):
uid
=
None
timestamp
=
datetime
.
datetime
.
utcnow
()
cached_token
=
config
.
db
.
authtokens
.
find_one
({
'
_id
'
:
access
_token
})
cached_token
=
config
.
db
.
authtokens
.
find_one
({
'
_id
'
:
session
_token
})
if
cached_token
:
uid
=
cached_token
[
'
uid
'
]
self
.
request
.
logger
.
debug
(
'
looked up cached token in %dms
'
,
((
datetime
.
datetime
.
utcnow
()
-
timestamp
).
total_seconds
()
*
1000.
))
else
:
try
:
auth_type
,
token
=
access_token
.
split
(
'
'
,
1
)
except
ValueError
:
# If token is not cached, user must provide auth type in header
self
.
abort
(
401
,
'
Auth type not provided with token
'
)
self
.
abort
(
401
,
'
Invalid session token
'
)
uid
=
self
.
validate_oauth_token
(
auth_type
,
token
,
timestamp
)
self
.
request
.
logger
.
debug
(
'
looked up remote token in %dms
'
,
((
datetime
.
datetime
.
utcnow
()
-
timestamp
).
total_seconds
()
*
1000.
))
return
uid
# Cache the token for future requests
update
=
{
'
uid
'
:
uid
,
'
timestamp
'
:
timestamp
,
'
auth_type
'
:
auth_type
}
dbutil
.
fault_tolerant_replace_one
(
'
authtokens
'
,
{
'
_id
'
:
token
},
update
,
upsert
=
True
)
return
uid
# def validate_oauth_token(self, auth_type, access_token, timestamp):
# """
# Validates a token assertion against the configured ID endpoint. Calls self.abort on failure.
# Returns the user's UID.
# """
# auth_config = config.get_auth(auth_type)
# id_endpoint = auth_config.get('id_endpoint')
# # If we start supporting more than google and ldap, break into classes inherited from abstract class
# if auth_type == 'google':
# r = requests.get(id_endpoint, headers={'Authorization': 'Bearer ' + access_token})
# elif auth_type == 'ldap':
# p = {'token': access_token}
# r = requests.post(id_endpoint, data=p)
# else:
# raise self.abort(401, 'Auth not configured.')
# if not r.ok:
# # Oauth authN failed; for now assume it was an invalid token. Could be more accurate in the future.
# err_msg = 'Invalid OAuth2 token.'
# site_id = config.get_item('site', 'id')
# headers = {'WWW-Authenticate': 'Bearer realm="{}", error="invalid_token", error_description="{}"'.format(site_id, err_msg)}
# self.request.logger.warning('{} Request headers: {}'.format(err_msg, str(self.request.headers.items())))
# self.abort(401, err_msg, headers=headers)
# identity = json.loads(r.content)
# email_key = 'email' if auth_type == 'google' else 'mail'
# uid = identity.get(email_key)
# if not uid:
# self.abort(400, 'OAuth2 token resolution did not return email address')
# # If this is the first time they've logged in, record that
# config.db.users.update_one({'_id': self.uid, 'firstlogin': None}, {'$set': {'firstlogin': timestamp}})
# # Unconditionally set their most recent login time
# config.db.users.update_one({'_id': self.uid}, {'$set': {'lastlogin': timestamp}})
# # Set user's auth provider avatar
# # TODO: switch on auth.provider rather than manually comparing endpoint URL.
# if auth_type == 'google':
# # A google-specific avatar URL is provided in the identity return.
# provider_avatar = identity.get('picture', '')
# # Remove attached size param from URL.
# u = urlparse.urlparse(provider_avatar)
# query = urlparse.parse_qs(u.query)
# query.pop('sz', None)
# u = u._replace(query=urllib.urlencode(query, True))
# provider_avatar = urlparse.urlunparse(u)
# # Update the user's provider avatar if it has changed.
# config.db.users.update_one({'_id': uid, 'avatars.provider': {'$ne': provider_avatar}}, {'$set':{'avatars.provider': provider_avatar, 'modified': timestamp}})
# # If the user has no avatar set, mark their provider_avatar as their chosen avatar.
# config.db.users.update_one({'_id': uid, 'avatar': {'$exists': False}}, {'$set':{'avatar': provider_avatar, 'modified': timestamp}})
# # Look to see if user has a Gravatar
# gravatar = util.resolve_gravatar(uid)
# if gravatar is not None:
# # Update the user's gravatar if it has changed.
# config.db.users.update_one({'_id': uid, 'avatars.gravatar': {'$ne': gravatar}}, {'$set':{'avatars.gravatar': gravatar, 'modified': timestamp}})
# return uid
def
validate_oauth_token
(
self
,
auth_type
,
access_token
,
timestamp
):
@log_access
(
AccessType
.
user_login
)
def
log_in
(
self
):
"""
Validates a token assertion against the configured ID endpoint. Calls self.abort on failure
.
Return succcess boolean if user successfully authenticates
.
Returns the user
'
s UID.
Used for access logging.
Not required to use system as logged in user.
"""
auth_config
=
config
.
get_auth
(
auth_type
)
id_endpoint
=
auth_config
.
get
(
'
id_endpoint
'
)
payload
=
self
.
request
.
json_body
if
'
code
'
not
in
payload
or
'
auth_type
'
not
in
payload
:
self
.
abort
(
400
,
'
Auth code and type required for login
'
)
# If we start supporting more than google and ldap, break into classes inherited from abstract class
if
auth_type
==
'
google
'
:
r
=
requests
.
get
(
id_endpoint
,
headers
=
{
'
Authorization
'
:
'
Bearer
'
+
access_token
})
elif
auth_type
==
'
ldap
'
:
p
=
{
'
token
'
:
access_token
}
r
=
requests
.
post
(
id_endpoint
,
data
=
p
)
else
:
raise
self
.
abort
(
401
,
'
Auth not configured.
'
)
if
not
r
.
ok
:
# Oauth authN failed; for now assume it was an invalid token. Could be more accurate in the future.
err_msg
=
'
Invalid OAuth2 token.
'
site_id
=
config
.
get_item
(
'
site
'
,
'
id
'
)
headers
=
{
'
WWW-Authenticate
'
:
'
Bearer realm=
"
{}
"
, error=
"
invalid_token
"
, error_description=
"
{}
"'
.
format
(
site_id
,
err_msg
)}
self
.
request
.
logger
.
warning
(
'
{} Request headers: {}
'
.
format
(
err_msg
,
str
(
self
.
request
.
headers
.
items
())))
self
.
abort
(
401
,
err_msg
,
headers
=
headers
)
try
:
auth_provider
=
AuthProvider
.
factory
(
payload
[
'
auth_type
'
])
except
NotImplementedError
as
e
:
self
.
abort
(
400
,
str
(
e
))
identity
=
json
.
loads
(
r
.
content
)
email_key
=
'
email
'
if
auth_type
==
'
google
'
else
'
mail
'
uid
=
identity
.
get
(
email_key
)
if
not
uid
:
self
.
abort
(
400
,
'
OAuth2 token resolution did not return email address
'
)
_
,
refresh_token
,
uid
=
auth_provider
.
validate_code
(
payload
[
'
code
'
])
# If this is the first time they've logged in, record that
config
.
db
.
users
.
update_one
({
'
_id
'
:
self
.
uid
,
'
firstlogin
'
:
None
},
{
'
$set
'
:
{
'
firstlogin
'
:
timestamp
}})
# Unconditionally set their most recent login time
config
.
db
.
users
.
update_one
({
'
_id
'
:
self
.
uid
},
{
'
$set
'
:
{
'
lastlogin
'
:
timestamp
}})
# Set user's auth provider avatar
# TODO: switch on auth.provider rather than manually comparing endpoint URL.
if
auth_type
==
'
google
'
:
# A google-specific avatar URL is provided in the identity return.
provider_avatar
=
identity
.
get
(
'
picture
'
,
''
)
# Remove attached size param from URL.
u
=
urlparse
.
urlparse
(
provider_avatar
)
query
=
urlparse
.
parse_qs
(
u
.
query
)
query
.
pop
(
'
sz
'
,
None
)
u
=
u
.
_replace
(
query
=
urllib
.
urlencode
(
query
,
True
))
provider_avatar
=
urlparse
.
urlunparse
(
u
)
# Update the user's provider avatar if it has changed.
config
.
db
.
users
.
update_one
({
'
_id
'
:
uid
,
'
avatars.provider
'
:
{
'
$ne
'
:
provider_avatar
}},
{
'
$set
'
:{
'
avatars.provider
'
:
provider_avatar
,
'
modified
'
:
timestamp
}})
# If the user has no avatar set, mark their provider_avatar as their chosen avatar.
config
.
db
.
users
.
update_one
({
'
_id
'
:
uid
,
'
avatar
'
:
{
'
$exists
'
:
False
}},
{
'
$set
'
:{
'
avatar
'
:
provider_avatar
,
'
modified
'
:
timestamp
}})
# Look to see if user has a Gravatar
gravatar
=
util
.
resolve_gravatar
(
uid
)
if
gravatar
is
not
None
:
# Update the user's gravatar if it has changed.
config
.
db
.
users
.
update_one
({
'
_id
'
:
uid
,
'
avatars.gravatar
'
:
{
'
$ne
'
:
gravatar
}},
{
'
$set
'
:{
'
avatars.gravatar
'
:
gravatar
,
'
modified
'
:
timestamp
}})
# Generate session token
session_token
=
base64
.
urlsafe_b64encode
(
os
.
urandom
(
42
))
return
uid
@log_access
(
AccessType
.
user_login
)
def
log_in
(
self
):
"""
Return succcess boolean if user successfully authenticates.
Used for access logging.
Not required to use system as logged in user.
"""
if
not
self
.
uid
:
self
.
abort
(
400
,
'
Only users may log in.
'
)
token_entry
=
{
'
token
'
:
session_token
,
'
refresh_token
'
:
refresh_token
'
uid
'
:
uid
,
'
timestamp
'
:
datetime
.
datetime
.
utcnow
(),
'
auth_type
'
:
auth_type
}
config
.
db
.
authtokens
.
insert_one
(
token_entry
)
return
{
'
success
'
:
True
}
return
{
'
token
'
:
session_token
}
@log_access
(
AccessType
.
user_logout
)
...
...
@@ -240,11 +253,12 @@ class RequestHandler(webapp2.RequestHandler):
Remove all cached auth tokens associated with caller
'
s uid.
"""
if
not
self
.
uid
:
self
.
abort
(
400
,
'
Only users may log out.
'
)
payload
=
self
.
request
.
json_body
if
'
token
'
not
in
payload
:
self
.
abort
(
400
,
'
Token required for log out
'
)
result
=
config
.
db
.
authtokens
.
delete_
many
({
'
u
id
'
:
self
.
uid
})
return
{
'
auth_
tokens_removed
'
:
result
.
deleted_count
}
result
=
config
.
db
.
authtokens
.
delete_
one
({
'
_
id
'
:
token
})
return
{
'
tokens_removed
'
:
result
.
deleted_count
}
def
set_origin
(
self
,
drone_request
):
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment