Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F6067764
rcmail_oauth.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
17 KB
Referenced Files
None
Subscribers
None
rcmail_oauth.php
View Options
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Roundcube OAuth2 utilities |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
use
GuzzleHttp\Client
;
use
GuzzleHttp\MessageFormatter
;
use
GuzzleHttp\Exception\RequestException
;
/**
* Roundcube OAuth2 utilities
*
* @package Webmail
* @subpackage Utils
*/
class
rcmail_oauth
{
/** @var rcmail */
protected
$rcmail
;
/** @var array */
protected
$options
=
[];
/** @var string */
protected
$last_error
=
null
;
/** @var boolean */
protected
$no_redirect
=
false
;
/** @var rcmail_oauth */
static
protected
$instance
;
/**
* Singleton factory
*
* @return rcmail_oauth The one and only instance
*/
static
function
get_instance
(
$options
=
[])
{
if
(!
self
::
$instance
)
{
self
::
$instance
=
new
rcmail_oauth
(
$options
);
self
::
$instance
->
init
();
}
return
self
::
$instance
;
}
/**
* Object constructor
*
* @param array $options Config options:
*/
public
function
__construct
(
$options
=
[])
{
$this
->
rcmail
=
rcmail
::
get_instance
();
$this
->
options
=
(
array
)
$options
+
[
'provider'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_provider'
),
'auth_uri'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_auth_uri'
),
'token_uri'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_token_uri'
),
'client_id'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_client_id'
),
'client_secret'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_client_secret'
),
'identity_uri'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_identity_uri'
),
'identity_fields'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_identity_fields'
,
[
'email'
]),
'scope'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_scope'
),
'verify_peer'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_verify_peer'
,
true
),
'auth_parameters'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_auth_parameters'
,
[]),
'login_redirect'
=>
$this
->
rcmail
->
config
->
get
(
'oauth_login_redirect'
,
false
),
];
}
/**
* Initialize this instance
*
* @return void
*/
protected
function
init
()
{
// subscrbe to storage and smtp init events
if
(
$this
->
is_enabled
())
{
$this
->
rcmail
->
plugins
->
register_hook
(
'storage_init'
,
[
$this
,
'storage_init'
]);
$this
->
rcmail
->
plugins
->
register_hook
(
'smtp_connect'
,
[
$this
,
'smtp_connect'
]);
$this
->
rcmail
->
plugins
->
register_hook
(
'logout_after'
,
[
$this
,
'logout_after'
]);
$this
->
rcmail
->
plugins
->
register_hook
(
'unauthenticated'
,
[
$this
,
'unauthenticated'
]);
}
}
/**
* Check if OAuth is generally enabled in config
*
* @return boolean
*/
public
function
is_enabled
()
{
return
!
empty
(
$this
->
options
[
'provider'
])
&&
!
empty
(
$this
->
options
[
'token_uri'
])
&&
!
empty
(
$this
->
options
[
'client_id'
]);
}
/**
* Compose a fully qualified redirect URI for auth requests
*
* @return string
*/
public
function
get_redirect_uri
()
{
// rewrite redirect URL to not contain query parameters because some providers do not support this
return
preg_replace
(
'/
\/
?
\?
_task=[a-z]+/'
,
'/index.php/login/oauth'
,
$this
->
rcmail
->
url
([],
true
,
true
));
}
/**
* Getter for the last error occured
*
* @return mixed
*/
public
function
get_last_error
()
{
return
$this
->
last_error
;
}
/**
* Helper method to decode a JWT
*
* @param string $jwt
* @return array Hash array with decoded body
*/
public
function
jwt_decode
(
$jwt
)
{
list
(
$headb64
,
$bodyb64
,
$cryptob64
)
=
explode
(
'.'
,
$jwt
);
$header
=
json_decode
(
base64_decode
(
$headb64
),
true
);
$body
=
json_decode
(
base64_decode
(
$bodyb64
),
true
);
if
(
isset
(
$body
[
'azp'
])
&&
$body
[
'azp'
]
!==
$this
->
options
[
'client_id'
])
{
throw
new
RuntimeException
(
'Failed to validate JWT: invalid azp value'
);
}
else
if
(
isset
(
$body
[
'aud'
])
&&
$body
[
'aud'
]
!==
$this
->
options
[
'client_id'
])
{
throw
new
RuntimeException
(
'Failed to validate JWT: invalid aud value'
);
}
else
if
(!
isset
(
$body
[
'azp'
])
&&
!
isset
(
$body
[
'aud'
]))
{
throw
new
RuntimeException
(
'Failed to validate JWT: missing aud/azp value'
);
}
return
$body
;
}
/**
* Login action: redirect to `oauth_auth_uri`
*
* @return void
*/
public
function
login_redirect
()
{
if
(!
empty
(
$this
->
options
[
'auth_uri'
])
&&
!
empty
(
$this
->
options
[
'client_id'
]))
{
// create a secret string
$_SESSION
[
'oauth_state'
]
=
rcube_utils
::
random_bytes
(
12
);
// compose full oauth login uri
$delimiter
=
strpos
(
$this
->
options
[
'auth_uri'
],
'?'
)
>
0
?
'&'
:
'?'
;
$query
=
http_build_query
([
'response_type'
=>
'code'
,
'client_id'
=>
$this
->
options
[
'client_id'
],
'scope'
=>
$this
->
options
[
'scope'
],
'redirect_uri'
=>
$this
->
get_redirect_uri
(),
'state'
=>
$_SESSION
[
'oauth_state'
],
]
+
(
array
)
$this
->
options
[
'auth_parameters'
]);
$this
->
rcmail
->
output
->
redirect
(
$this
->
options
[
'auth_uri'
]
.
$delimiter
.
$query
);
// exit
}
else
{
// log error about missing config options
rcube
::
raise_error
([
'message'
=>
"Missing required OAuth config options 'oauth_auth_uri', 'oauth_client_id'"
,
'file'
=>
__FILE__
,
'line'
=>
__LINE__
,
],
true
,
false
);
}
}
/**
* Request access token with auth code returned from oauth login
*
* @param string $auth_code
* @param string $state
*
* @return array Authorization data as hash array with entries
* `username` as the authentication user name
* `authorization` as the oauth authorization string "<type> <access-token>"
* `token` as the complete oauth response to be stored in session
*/
public
function
request_access_token
(
$auth_code
,
$state
=
null
)
{
$oauth_token_uri
=
$this
->
options
[
'token_uri'
];
$oauth_client_id
=
$this
->
options
[
'client_id'
];
$oauth_client_secret
=
$this
->
options
[
'client_secret'
];
$oauth_identity_uri
=
$this
->
options
[
'identity_uri'
];
if
(!
empty
(
$oauth_token_uri
)
&&
!
empty
(
$oauth_client_secret
))
{
// validate state parameter against $_SESSION['oauth_state']
if
(!
empty
(
$_SESSION
[
'oauth_state'
])
&&
$_SESSION
[
'oauth_state'
]
!==
$state
)
{
throw
new
RuntimeException
(
'Invalid state parameter'
);
}
// send token request to get a real access token for the given auth code
try
{
$client
=
new
Client
([
'timeout'
=>
10.0
,
'verify'
=>
$this
->
options
[
'verify_peer'
],
]);
$response
=
$client
->
post
(
$oauth_token_uri
,
[
'form_params'
=>
[
'code'
=>
$auth_code
,
'client_id'
=>
$oauth_client_id
,
'client_secret'
=>
$oauth_client_secret
,
'redirect_uri'
=>
$this
->
get_redirect_uri
(),
'grant_type'
=>
'authorization_code'
,
],
]);
$data
=
\GuzzleHttp\json_decode
(
$response
->
getBody
(),
true
);
// auth success
if
(!
empty
(
$data
[
'access_token'
]))
{
$username
=
null
;
$authorization
=
sprintf
(
'%s %s'
,
$data
[
'token_type'
],
$data
[
'access_token'
]);
// decode JWT id_token if provided
if
(!
empty
(
$data
[
'id_token'
]))
{
try
{
$identity
=
$this
->
jwt_decode
(
$data
[
'id_token'
]);
foreach
(
$this
->
options
[
'identity_fields'
]
as
$field
)
{
if
(
isset
(
$identity
[
$field
]))
{
$username
=
$identity
[
$field
];
unset
(
$data
[
'id_token'
]);
break
;
}
}
}
catch
(
\Exception
$e
)
{
// log error
rcube
::
raise_error
([
'message'
=>
$e
->
getMessage
(),
'file'
=>
__FILE__
,
'line'
=>
__LINE__
,
],
true
,
false
);
}
}
// request user identity (email)
if
(
empty
(
$username
)
&&
!
empty
(
$oauth_identity_uri
))
{
$identity_response
=
$client
->
get
(
$oauth_identity_uri
,
[
'headers'
=>
[
'Authorization'
=>
$authorization
,
'Accept'
=>
'application/json'
,
],
]);
$identity
=
\GuzzleHttp\json_decode
(
$identity_response
->
getBody
(),
true
);
foreach
(
$this
->
options
[
'identity_fields'
]
as
$field
)
{
if
(
isset
(
$identity
[
$field
]))
{
$username
=
$identity
[
$field
];
break
;
}
}
}
$data
[
'identity'
]
=
$username
;
$this
->
mask_auth_data
(
$data
);
$this
->
rcmail
->
session
->
remove
(
'oauth_state'
);
// return auth data
return
[
'username'
=>
$username
,
'authorization'
=>
$authorization
,
'token'
=>
$data
,
];
}
else
{
throw
new
Exception
(
'Unexpected response from OAuth service'
);
}
}
catch
(
RequestException
$e
)
{
$this
->
last_error
=
"OAuth token request failed: "
.
$e
->
getMessage
();
$this
->
no_redirect
=
true
;
$formatter
=
new
MessageFormatter
();
rcube
::
raise_error
([
'message'
=>
$this
->
last_error
.
'; '
.
$formatter
->
format
(
$e
->
getRequest
(),
$e
->
getResponse
()),
'file'
=>
__FILE__
,
'line'
=>
__LINE__
,
],
true
,
false
);
return
false
;
}
catch
(
Exception
$e
)
{
$this
->
last_error
=
"OAuth token request failed: "
.
$e
->
getMessage
();
$this
->
no_redirect
=
true
;
rcube
::
raise_error
([
'message'
=>
$this
->
last_error
,
'file'
=>
__FILE__
,
'line'
=>
__LINE__
,
],
true
,
false
);
return
false
;
}
}
else
{
$this
->
last_error
=
"Missing required OAuth config options 'oauth_token_uri', 'oauth_client_id', 'oauth_client_secret'"
;
rcube
::
raise_error
([
'message'
=>
$this
->
last_error
,
'file'
=>
__FILE__
,
'line'
=>
__LINE__
,
],
true
,
false
);
return
false
;
}
}
/**
* Obtain a new access token using the refresh_token grant type
*
* If successful, this will update the `oauth_token` entry in
* session data.
*
* @param array $token
*
* @return array Updated authorization data
*/
public
function
refresh_access_token
(
array
$token
)
{
$oauth_token_uri
=
$this
->
options
[
'token_uri'
];
$oauth_client_id
=
$this
->
options
[
'client_id'
];
$oauth_client_secret
=
$this
->
options
[
'client_secret'
];
// send token request to get a real access token for the given auth code
try
{
$client
=
new
Client
([
'timeout'
=>
10.0
,
'verify'
=>
$this
->
options
[
'verify_peer'
],
]);
$response
=
$client
->
post
(
$oauth_token_uri
,
[
'form_params'
=>
[
'client_id'
=>
$oauth_client_id
,
'client_secret'
=>
$oauth_client_secret
,
'refresh_token'
=>
$this
->
rcmail
->
decrypt
(
$token
[
'refresh_token'
]),
'grant_type'
=>
'refresh_token'
,
],
]);
$data
=
\GuzzleHttp\json_decode
(
$response
->
getBody
(),
true
);
// auth success
if
(!
empty
(
$data
[
'access_token'
]))
{
// update access token stored as password
$authorization
=
sprintf
(
'%s %s'
,
$data
[
'token_type'
],
$data
[
'access_token'
]);
$_SESSION
[
'password'
]
=
$this
->
rcmail
->
encrypt
(
$authorization
);
$this
->
mask_auth_data
(
$data
);
// update session data
$_SESSION
[
'oauth_token'
]
=
array_merge
(
$token
,
$data
);
return
[
'token'
=>
$data
,
'authorization'
=>
$authorization
,
];
}
}
catch
(
RequestException
$e
)
{
$this
->
last_error
=
"OAuth refresh token request failed: "
.
$e
->
getMessage
();
$formatter
=
new
MessageFormatter
();
rcube
::
raise_error
([
'message'
=>
$this
->
last_error
.
'; '
.
$formatter
->
format
(
$e
->
getRequest
(),
$e
->
getResponse
()),
'file'
=>
__FILE__
,
'line'
=>
__LINE__
,
],
true
,
false
);
return
false
;
}
catch
(
Exception
$e
)
{
$this
->
last_error
=
"OAuth refresh token request failed: "
.
$e
->
getMessage
();
rcube
::
raise_error
([
'message'
=>
$this
->
last_error
,
'file'
=>
__FILE__
,
'line'
=>
__LINE__
,
],
true
,
false
);
return
false
;
}
}
/**
* Modify some properties of the received auth response
*
* @param array $token
* @return void
*/
protected
function
mask_auth_data
(&
$data
)
{
// compute absolute token expiration date
$data
[
'expires'
]
=
time
()
+
$data
[
'expires_in'
]
-
600
;
// encrypt refresh token if provided
if
(
isset
(
$data
[
'refresh_token'
]))
{
$data
[
'refresh_token'
]
=
$this
->
rcmail
->
encrypt
(
$data
[
'refresh_token'
]);
}
}
/**
* Check the given access token data if still valid
*
* ... and attempt to refresh if possible.
*
* @param array $token
* @return void
*/
protected
function
check_token_validity
(
$token
)
{
if
(
$token
[
'expires'
]
<
time
()
&&
isset
(
$token
[
'refresh_token'
]))
{
$this
->
refresh_access_token
(
$token
);
}
}
/**
* Callback for 'storage_init' hook
*
* @param array $options
* @return array
*/
public
function
storage_init
(
$options
)
{
if
(
isset
(
$_SESSION
[
'oauth_token'
])
&&
$options
[
'driver'
]
===
'imap'
)
{
// check token validity
$this
->
check_token_validity
(
$_SESSION
[
'oauth_token'
]);
// enforce XOAUTH2 authorization type
$options
[
'auth_type'
]
=
'XOAUTH2'
;
}
return
$options
;
}
/**
* Callback for 'smtp_connect' hook
*
* @param array $options
* @return array
*/
public
function
smtp_connect
(
$options
)
{
if
(
isset
(
$_SESSION
[
'oauth_token'
]))
{
// check token validity
$this
->
check_token_validity
(
$_SESSION
[
'oauth_token'
]);
// enforce XOAUTH2 authorization type
$options
[
'smtp_user'
]
=
'%u'
;
$options
[
'smtp_pass'
]
=
'%p'
;
$options
[
'smtp_auth_type'
]
=
'XOAUTH2'
;
}
return
$options
;
}
/**
* Callback for 'logout_after' hook
*
* @param array $options
* @return array
*/
public
function
logout_after
(
$options
)
{
$this
->
no_redirect
=
true
;
}
/**
* Callback for 'unauthenticated' hook
*
* @param array $options
* @return array
*/
public
function
unauthenticated
(
$options
)
{
if
(
$this
->
options
[
'login_redirect'
]
&&
!
$this
->
rcmail
->
output
->
ajax_call
&&
!
$this
->
no_redirect
&&
empty
(
$options
[
'error'
])
&&
$options
[
'http_code'
]
===
200
)
{
$this
->
login_redirect
();
}
return
$options
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, May 22, 5:24 AM (23 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
782804
Default Alt Text
rcmail_oauth.php (17 KB)
Attached To
Mode
R3 roundcubemail
Attached
Detach File
Event Timeline
Log In to Comment