Spike - Generic OAuth
OVERVIEW
OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices
We currently support three OAuth Providers, GitLab, Google, and Office365. Each of these have separate implementations for the following reasons, scopes, endpoints and user information.
In order to support “Generic OAuth” each supported OAuth provider would have to support the following configuration settings.
Scope:
OAuth does not define scopes, those definitions are left up to the OAuth Provider. However, for our purposes, scopes could be requested and stored in the configuration file. For our existing OAuth providers the scopes, though different are hardcoded for each provider in the configuration.
Endpoints:
Our implementation requires three endpoints, the Authorization Endpoint, the Token Endpoint and the User Endpoint. These are also already stored in the configuration file as well. Again, they are hardcoded base on the provider. Some providers require additional information about the server url.
User Information:
The user_info endpoint is the most complicated to solve from a generic standpoint. This is because the return from the user information endpoint is not standard. Therefore, each OAuth provider may return a different JSON object.
If most providers return a “key/value” list parsing and mapping will be pretty straight forward. Simply require the admin to map the values we wish to retrieve to the values returned in the JSON object.
{
"id":4570140,
"name":"Scott Bishel",
"username":"sbishel",
"state":"active",
"avatar_url":"https://secure.gravatar.com/avatar/0a6b33bc4d0e6767c5498a4e3a89dab0?s=80\u0026d=identicon",
"web_url":"https://gitlab.com/sbishel",
"created_at":"2019-09-05T19:12:11.418Z",
"bio":null,
"location":null,
"public_email":"",
"skype":"",
"linkedin":"",
"twitter":"",
"website_url":"",
"organization":null,
"job_title":"",
"work_information":null,
"last_sign_in_at":"2019-10-08T16:11:26.701Z",
"confirmed_at":"2019-09-05T19:12:39.972Z",
"last_activity_on":"2020-07-07",
"email":"scott.bishel@mattermost.com",
"theme_id":1,
"color_scheme_id":1,
"projects_limit":100000,
"current_sign_in_at":"2020-07-02T02:22:14.428Z",
"identities":[],
"can_create_group":true,
"can_create_project":true,
"two_factor_enabled":true,
"external":false,
"private_profile":false,
"shared_runners_minutes_limit":null,
"extra_shared_runners_minutes_limit":null
}
However, Google (and perhaps others) return a more complex JSON object. Determining how to parse this return value could cause some issues.
{
"resourceName": "people/110216300263091673461",
"etag": "%EggBAj0JPgo3LhoEAQIFBw==",
"metadata": {
"sources": [
{
"type": "PROFILE",
"id": "110216300263091673461",
"etag": "#abtg32Cr/eg=",
"profileMetadata": {
"objectType": "PERSON",
"userTypes": [
"GOOGLE_USER",
"GOOGLE_APPS_USER"
]
}
},
{
"type": "DOMAIN_PROFILE",
"id": "110216300263091673461",
"etag": "#abtg32Cr/eg="
}
],
"objectType": "PERSON"
},
"names": [
{
"metadata": {
"primary": true,
"source": {
"type": "PROFILE",
"id": "110216300263091673461"
}
},
"displayName": "Scott Bishel",
"familyName": "Bishel",
"givenName": "Scott",
"displayNameLastFirst": "Bishel, Scott",
"unstructuredName": "Scott Bishel"
},
{
"metadata": {
"source": {
"type": "DOMAIN_PROFILE",
"id": "110216300263091673461"
}
},
"displayName": "Scott Bishel",
"familyName": "Bishel",
"givenName": "Scott",
"displayNameLastFirst": "Scott Bishel",
"unstructuredName": "Scott Bishel"
}
],
"emailAddresses": [
{
"metadata": {
"primary": true,
"verified": true,
"source": {
"type": "DOMAIN_PROFILE",
"id": "110216300263091673461"
}
},
"value": "scott.bishel@mattermost.com"
},
{
"metadata": {
"verified": true,
"source": {
"type": "DOMAIN_PROFILE",
"id": "110216300263091673461"
}
},
"value": "scott@mattermost.com"
}
]
}
One possibility would be to use a library like github.com/PaesslerAG/jsonpath
This library supports access a JSON document via a path. Similar to the way XPath allows access to an XML document. The administrator would then need to set up mappings via jsonPath settings.
var gi interface{}
err := decoder.Decode(&gi)
if err == nil {
emails, _ := jsonpath.Get("$.emailAddresses[0].value", gi)
userName, _ := jsonpath.Get("$.emailAddresses[0].value", gi)
firstName, _ := jsonpath.Get("$.names[0].givenName", gi)
lastName, _ := jsonpath.Get("$.names[0].familyName", gi)
nicknames, _ := jsonpath.Get("$.nicknames[0].value", gi)
authData, _ := jsonpath.Get("$.metadata.sources[0].id", gi)
authService := USER_AUTH_SERVICE_GOOGLE
}
OpenId Connect
OpenId Connect was designed to solve this problem. OpenId Connect standardizes areas that OAuth 2.0 leaves up to choice, such as scopes and endpoint discovery. See the Tech Spec - OpenId Connect for more information.
WebApp Changes
In order to support Generic OAuth, a new OAuth provider will be added. In addition to the Endpoint that get defined for OAuth the Generic OAuth Setting will need to support mapping these user fields.
email
userName
firstName
lastName
nickname
authData
The five or six most popular OAuth providers should be identified to determine if most return a simple key/value listing or a more complex object graph from the user endpoint. We will need to determine which mapping method would prove most effective.
Server Changes
The server will need to handle a different configuration, the existing OAuth Config settings, plus settings to control the mapping of the user information JSON.
"GenericOAuthSettings":{
"Name": "My Generic OAuth Provider"
"Enable": false,
"Secret": "3d4651465eff9493271e9146de8ff6d658b09787aaa873ff03533188b68722da",
"Id": "4aee457b0176327fd58aae5339cd7dabdd1c2d36637d1b8766a6e25da94e3622",
"Scope": "",
"AuthEndpoint": "https://gitlab.com/oauth/authorize",
"TokenEndpoint": "https://gitlab.com/oauth/token",
"UserApiEndpoint": "https://gitlab.com/api/v4/user"
"Email":"email"
"FirstName":"names/firstname"
"LastName":"names/lastname"
"NickName":"names/nickname"
"AuthData":"id"
}
Enterprise Changes
The Enterprise is where the implementation for the new Generic OAuth provider will preside. It will implement the OAuthProvider Interface (GetUserFromJSON()). It will need to create a model.User object based off the mappings in the configuration file and the body returned from the UserInfo Endpoint.
The init() will need to be sure to register the new OAuth Provider.
WebApp/Mobile Changes
Both WebApp and Mobile will need to support the “generic” OAuth provider when logging in. Mobile currently doesn’t support Google OAuth. We need to make sure it's not a technical limitation and will not impact the ability for a generic provider.
The user Account Settings will also need modified to ensure that neither the username nor the email can be set by the user. Currently, the LastName/FirstName can be update, but get overwritten, so these fields should probably turned off as well. However, there is no guarantee they are available from an OAuth provider.
Licensing
The Generic OAuth provider is planned as a E20 feature. The feature should be protected by license checks.
Permissions
There should not be any permission changes.
Schema
No Schema Changes. New AuthType
in Users table, but that is set in server and a simple string in the database.
REST API
No additional Rest API changes
CLI
No CLI changes required