mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-12 23:43:23 +00:00
Compare commits
16 Commits
45fe1bd1df
...
e54e3ba8ca
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e54e3ba8ca | ||
![]() |
3b52dca405 | ||
![]() |
853a0c466e | ||
![]() |
a2a34fbadd | ||
![]() |
677e0b8a37 | ||
![]() |
81b79fefb7 | ||
![]() |
9e0eb99747 | ||
![]() |
fcd238cb4b | ||
![]() |
463d5dd4d5 | ||
![]() |
1af6ae19b9 | ||
![]() |
7898619d74 | ||
![]() |
2250aead46 | ||
![]() |
47e4f8478f | ||
![]() |
fad8f7b148 | ||
![]() |
b21e7d8fdb | ||
![]() |
5c6ad1a0e5 |
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
class Api::V1::AppsController < Api::BaseController
|
class Api::V1::AppsController < Api::BaseController
|
||||||
skip_before_action :require_authenticated_user!
|
skip_before_action :require_authenticated_user!
|
||||||
|
before_action :validate_token_endpoint_auth_method!
|
||||||
|
|
||||||
|
TOKEN_ENDPOINT_AUTH_METHODS = %w(none client_secret_basic client_secret_post).freeze
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@app = Doorkeeper::Application.create!(application_options)
|
@app = Doorkeeper::Application.create!(application_options)
|
||||||
|
@ -16,14 +19,25 @@ class Api::V1::AppsController < Api::BaseController
|
||||||
redirect_uri: app_params[:redirect_uris],
|
redirect_uri: app_params[:redirect_uris],
|
||||||
scopes: app_scopes_or_default,
|
scopes: app_scopes_or_default,
|
||||||
website: app_params[:website],
|
website: app_params[:website],
|
||||||
|
confidential: !app_public?,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_token_endpoint_auth_method!
|
||||||
|
return unless app_params.include? :token_endpoint_auth_method
|
||||||
|
|
||||||
|
bad_request unless TOKEN_ENDPOINT_AUTH_METHODS.include? app_params[:token_endpoint_auth_method]
|
||||||
|
end
|
||||||
|
|
||||||
|
def app_public?
|
||||||
|
app_params[:token_endpoint_auth_method] == 'none'
|
||||||
|
end
|
||||||
|
|
||||||
def app_scopes_or_default
|
def app_scopes_or_default
|
||||||
app_params[:scopes] || Doorkeeper.configuration.default_scopes
|
app_params[:scopes] || Doorkeeper.configuration.default_scopes
|
||||||
end
|
end
|
||||||
|
|
||||||
def app_params
|
def app_params
|
||||||
params.permit(:client_name, :scopes, :website, :redirect_uris, redirect_uris: [])
|
params.permit(:client_name, :scopes, :website, :token_endpoint_auth_method, :redirect_uris, redirect_uris: [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,13 @@ class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_success
|
def render_success
|
||||||
if skip_authorization? || (matching_token? && !truthy_param?('force_login'))
|
# FIXME: Find a better way to apply this validation: if the scopes only
|
||||||
|
# includes offline_access, then it's not valid, since offline_access doesn't
|
||||||
|
# actually give access to resources:
|
||||||
|
if pre_auth.scopes.all?('offline_access')
|
||||||
|
error = Doorkeeper::OAuth::InvalidRequestResponse.new(reason: :offline_access_only, missing_param: nil)
|
||||||
|
render :error, locals: { error_response: error }, status: 400
|
||||||
|
elsif skip_authorization? || (matching_token? && !truthy_param?('force_login'))
|
||||||
redirect_or_render authorize_response
|
redirect_or_render authorize_response
|
||||||
elsif Doorkeeper.configuration.api_only
|
elsif Doorkeeper.configuration.api_only
|
||||||
render json: pre_auth
|
render json: pre_auth
|
||||||
|
|
|
@ -26,6 +26,12 @@ module ContextHelper
|
||||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
||||||
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
||||||
|
quotes: {
|
||||||
|
'quote' => 'https://w3id.org/fep/044f#quote',
|
||||||
|
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
|
||||||
|
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
|
||||||
|
'quoteAuthorization' => 'https://w3id.org/fep/044f#quoteAuthorization',
|
||||||
|
},
|
||||||
interaction_policies: {
|
interaction_policies: {
|
||||||
'gts' => 'https://gotosocial.org/ns#',
|
'gts' => 'https://gotosocial.org/ns#',
|
||||||
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
|
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },
|
||||||
|
|
|
@ -1,12 +1,30 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||||
|
|
||||||
export const AccountBio: React.FC<{
|
interface AccountBioProps {
|
||||||
note: string;
|
note: string;
|
||||||
className: string;
|
className: string;
|
||||||
}> = ({ note, className }) => {
|
dropdownAccountId?: string;
|
||||||
const handleClick = useLinks();
|
}
|
||||||
|
|
||||||
if (note.length === 0 || note === '<p></p>') {
|
export const AccountBio: React.FC<AccountBioProps> = ({
|
||||||
|
note,
|
||||||
|
className,
|
||||||
|
dropdownAccountId,
|
||||||
|
}) => {
|
||||||
|
const handleClick = useLinks(!!dropdownAccountId);
|
||||||
|
const handleNodeChange = useCallback(
|
||||||
|
(node: HTMLDivElement | null) => {
|
||||||
|
if (!dropdownAccountId || !node || node.childNodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addDropdownToHashtags(node, dropdownAccountId);
|
||||||
|
},
|
||||||
|
[dropdownAccountId],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (note.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +33,28 @@ export const AccountBio: React.FC<{
|
||||||
className={`${className} translate`}
|
className={`${className} translate`}
|
||||||
dangerouslySetInnerHTML={{ __html: note }}
|
dangerouslySetInnerHTML={{ __html: note }}
|
||||||
onClickCapture={handleClick}
|
onClickCapture={handleClick}
|
||||||
|
ref={handleNodeChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const childNode of node.childNodes) {
|
||||||
|
if (!(childNode instanceof HTMLElement)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
childNode instanceof HTMLAnchorElement &&
|
||||||
|
(childNode.classList.contains('hashtag') ||
|
||||||
|
childNode.innerText.startsWith('#')) &&
|
||||||
|
!childNode.dataset.menuHashtag
|
||||||
|
) {
|
||||||
|
childNode.dataset.menuHashtag = accountId;
|
||||||
|
} else if (childNode.childNodes.length > 0) {
|
||||||
|
addDropdownToHashtags(childNode, accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
@ -773,7 +774,6 @@ export const AccountHeader: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = { __html: account.note_emojified };
|
|
||||||
const displayNameHtml = { __html: account.display_name_html };
|
const displayNameHtml = { __html: account.display_name_html };
|
||||||
const fields = account.fields;
|
const fields = account.fields;
|
||||||
const isLocal = !account.acct.includes('@');
|
const isLocal = !account.acct.includes('@');
|
||||||
|
@ -897,12 +897,11 @@ export const AccountHeader: React.FC<{
|
||||||
<AccountNote accountId={accountId} />
|
<AccountNote accountId={accountId} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{account.note.length > 0 && account.note !== '<p></p>' && (
|
<AccountBio
|
||||||
<div
|
note={account.note_emojified}
|
||||||
className='account__header__content translate'
|
dropdownAccountId={accountId}
|
||||||
dangerouslySetInnerHTML={content}
|
className='account__header__content'
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='account__header__fields'>
|
<div className='account__header__fields'>
|
||||||
<dl>
|
<dl>
|
||||||
|
|
|
@ -8,13 +8,14 @@ import { openURL } from 'mastodon/actions/search';
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
const isMentionClick = (element: HTMLAnchorElement) =>
|
const isMentionClick = (element: HTMLAnchorElement) =>
|
||||||
element.classList.contains('mention');
|
element.classList.contains('mention') &&
|
||||||
|
!element.classList.contains('hashtag');
|
||||||
|
|
||||||
const isHashtagClick = (element: HTMLAnchorElement) =>
|
const isHashtagClick = (element: HTMLAnchorElement) =>
|
||||||
element.textContent?.[0] === '#' ||
|
element.textContent?.[0] === '#' ||
|
||||||
element.previousSibling?.textContent?.endsWith('#');
|
element.previousSibling?.textContent?.endsWith('#');
|
||||||
|
|
||||||
export const useLinks = () => {
|
export const useLinks = (skipHashtags?: boolean) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
@ -61,12 +62,12 @@ export const useLinks = () => {
|
||||||
if (isMentionClick(target)) {
|
if (isMentionClick(target)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void handleMentionClick(target);
|
void handleMentionClick(target);
|
||||||
} else if (isHashtagClick(target)) {
|
} else if (isHashtagClick(target) && !skipHashtags) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleHashtagClick(target);
|
handleHashtagClick(target);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleMentionClick, handleHashtagClick],
|
[skipHashtags, handleMentionClick, handleHashtagClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
return handleClick;
|
return handleClick;
|
||||||
|
|
|
@ -126,6 +126,9 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
? accountJSON.username
|
? accountJSON.username
|
||||||
: accountJSON.display_name;
|
: accountJSON.display_name;
|
||||||
|
|
||||||
|
const accountNote =
|
||||||
|
accountJSON.note && accountJSON.note !== '<p></p>' ? accountJSON.note : '';
|
||||||
|
|
||||||
return AccountFactory({
|
return AccountFactory({
|
||||||
...accountJSON,
|
...accountJSON,
|
||||||
moved: moved?.id,
|
moved: moved?.id,
|
||||||
|
@ -142,8 +145,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
|
||||||
escapeTextContentForBrowser(displayName),
|
escapeTextContentForBrowser(displayName),
|
||||||
emojiMap,
|
emojiMap,
|
||||||
),
|
),
|
||||||
note_emojified: emojify(accountJSON.note, emojiMap),
|
note_emojified: emojify(accountNote, emojiMap),
|
||||||
note_plain: unescapeHTML(accountJSON.note),
|
note_plain: unescapeHTML(accountNote),
|
||||||
url:
|
url:
|
||||||
accountJSON.url.startsWith('http://') ||
|
accountJSON.url.startsWith('http://') ||
|
||||||
accountJSON.url.startsWith('https://')
|
accountJSON.url.startsWith('https://')
|
||||||
|
|
|
@ -14,6 +14,8 @@ class ScopeTransformer < Parslet::Transform
|
||||||
|
|
||||||
# # override for profile scope which is read only
|
# # override for profile scope which is read only
|
||||||
@access = %w(read) if @term == 'profile'
|
@access = %w(read) if @term == 'profile'
|
||||||
|
# Override offline_access since it doesn't imply read or write access:
|
||||||
|
@access = %w(offline) if @term == 'offline_access'
|
||||||
end
|
end
|
||||||
|
|
||||||
def key
|
def key
|
||||||
|
|
|
@ -65,12 +65,22 @@ class SessionActivation < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def access_token_attributes
|
def access_token_attributes
|
||||||
|
app = Doorkeeper::Application.find_by(superapp: true)
|
||||||
|
scopes = Doorkeeper::OAuth::Scopes.from_array(DEFAULT_SCOPES)
|
||||||
|
|
||||||
|
context = Doorkeeper::OAuth::Authorization::Token.build_context(
|
||||||
|
app,
|
||||||
|
Doorkeeper::OAuth::AUTHORIZATION_CODE,
|
||||||
|
scopes,
|
||||||
|
user_id
|
||||||
|
)
|
||||||
|
|
||||||
{
|
{
|
||||||
application_id: Doorkeeper::Application.find_by(superapp: true)&.id,
|
application_id: context.client.id,
|
||||||
resource_owner_id: user_id,
|
resource_owner_id: context.resource_owner,
|
||||||
scopes: DEFAULT_SCOPES.join(' '),
|
scopes: context.scopes,
|
||||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
expires_in: Doorkeeper::OAuth::Authorization::Token.access_token_expires_in(Doorkeeper.config, context),
|
||||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?,
|
use_refresh_token: Doorkeeper::OAuth::Authorization::Token.refresh_token_enabled?(Doorkeeper.config, context),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -314,10 +314,12 @@ class User < ApplicationRecord
|
||||||
def token_for_app(app)
|
def token_for_app(app)
|
||||||
return nil if app.nil? || app.owner != self
|
return nil if app.nil? || app.owner != self
|
||||||
|
|
||||||
Doorkeeper::AccessToken.find_or_create_by(application_id: app.id, resource_owner_id: id) do |t|
|
context = Doorkeeper::OAuth::Authorization::Token.build_context(app, Doorkeeper::OAuth::AUTHORIZATION_CODE, app.scopes, app.owner.id)
|
||||||
t.scopes = app.scopes
|
|
||||||
t.expires_in = Doorkeeper.configuration.access_token_expires_in
|
Doorkeeper::AccessToken.find_or_create_by(application_id: context.client.id, resource_owner_id: context.resource_owner) do |t|
|
||||||
t.use_refresh_token = Doorkeeper.configuration.refresh_token_enabled?
|
t.scopes = context.scopes
|
||||||
|
t.expires_in = Doorkeeper::OAuth::Authorization::Token.access_token_expires_in(Doorkeeper.config, context)
|
||||||
|
t.use_refresh_token = Doorkeeper::OAuth::Authorization::Token.refresh_token_enabled?(Doorkeeper.config, context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -61,7 +61,7 @@ class OAuthMetadataPresenter < ActiveModelSerializers::Model
|
||||||
end
|
end
|
||||||
|
|
||||||
def token_endpoint_auth_methods_supported
|
def token_endpoint_auth_methods_supported
|
||||||
%w(client_secret_basic client_secret_post)
|
%w(none client_secret_basic client_secret_post)
|
||||||
end
|
end
|
||||||
|
|
||||||
def code_challenge_methods_supported
|
def code_challenge_methods_supported
|
||||||
|
|
|
@ -8,7 +8,7 @@ class REST::CredentialApplicationSerializer < REST::ApplicationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def client_secret
|
def client_secret
|
||||||
object.secret
|
object.secret if object.confidential?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Added for future forwards compatibility when we may decide to expire OAuth
|
# Added for future forwards compatibility when we may decide to expire OAuth
|
||||||
|
|
|
@ -27,12 +27,14 @@ class AppSignUpService < BaseService
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_access_token!
|
def create_access_token!
|
||||||
|
context = Doorkeeper::OAuth::Authorization::Token.build_context(@app, Doorkeeper::OAuth::AUTHORIZATION_CODE, @app.scopes, @user.id)
|
||||||
|
|
||||||
@access_token = Doorkeeper::AccessToken.create!(
|
@access_token = Doorkeeper::AccessToken.create!(
|
||||||
application: @app,
|
application: context.client,
|
||||||
resource_owner_id: @user.id,
|
resource_owner_id: context.resource_owner,
|
||||||
scopes: @app.scopes,
|
scopes: context.scopes,
|
||||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
expires_in: Doorkeeper::OAuth::Authorization::Token.access_token_expires_in(Doorkeeper.config, context),
|
||||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
|
use_refresh_token: Doorkeeper::OAuth::Authorization::Token.refresh_token_enabled?(Doorkeeper.config, context)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -31,10 +31,30 @@ Doorkeeper.configure do
|
||||||
# If you want to disable expiration, set this to nil.
|
# If you want to disable expiration, set this to nil.
|
||||||
access_token_expires_in nil
|
access_token_expires_in nil
|
||||||
|
|
||||||
# Assign a custom TTL for implicit grants.
|
# context.grant_type to compare with Doorkeeper::OAUTH grant type constants
|
||||||
# custom_access_token_expires_in do |oauth_client|
|
# context.client for client (Doorkeeper::Application)
|
||||||
# oauth_client.application.additional_settings.implicit_oauth_expiration
|
# context.scopes for scopes
|
||||||
# end
|
custom_access_token_expires_in do |context|
|
||||||
|
# If the client is confidential (all clients pre 4.3) and it hasn't
|
||||||
|
# requested offline_access, then we don't want to expire access tokens.
|
||||||
|
# Applications created by users are also considered confidential.
|
||||||
|
if context.client.confidential? && !context.scopes.exists?('offline_access')
|
||||||
|
nil
|
||||||
|
else
|
||||||
|
15.minutes.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
use_refresh_token do |context|
|
||||||
|
context.scopes.exists?('offline_access')
|
||||||
|
end
|
||||||
|
|
||||||
|
after_successful_strategy_response do |request, _response|
|
||||||
|
if request.is_a? Doorkeeper::OAuth::RefreshTokenRequest
|
||||||
|
Web::PushSubscription.where(access_token_id: request.refresh_token.id).update!(access_token_id: request.access_token.id)
|
||||||
|
SessionActivation.where(access_token_id: request.refresh_token.id).update!(access_token_id: request.access_token.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Use a custom class for generating the access token.
|
# Use a custom class for generating the access token.
|
||||||
# https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator
|
# https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator
|
||||||
|
@ -71,6 +91,7 @@ Doorkeeper.configure do
|
||||||
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
|
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
|
||||||
default_scopes :read
|
default_scopes :read
|
||||||
optional_scopes :profile,
|
optional_scopes :profile,
|
||||||
|
:offline_access,
|
||||||
:write,
|
:write,
|
||||||
:'write:accounts',
|
:'write:accounts',
|
||||||
:'write:blocks',
|
:'write:blocks',
|
||||||
|
@ -120,7 +141,9 @@ Doorkeeper.configure do
|
||||||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||||
# falls back to the `:client_id` and `:client_secret` params from the `params` object.
|
# falls back to the `:client_id` and `:client_secret` params from the `params` object.
|
||||||
# Check out the wiki for more information on customization
|
# Check out the wiki for more information on customization
|
||||||
# client_credentials :from_basic, :from_params
|
#
|
||||||
|
# This is the default value:
|
||||||
|
client_credentials :from_basic, :from_params
|
||||||
|
|
||||||
# Change the way access token is authenticated from the request object.
|
# Change the way access token is authenticated from the request object.
|
||||||
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
|
||||||
|
@ -165,7 +188,17 @@ Doorkeeper.configure do
|
||||||
# http://tools.ietf.org/html/rfc6819#section-4.4.3
|
# http://tools.ietf.org/html/rfc6819#section-4.4.3
|
||||||
#
|
#
|
||||||
|
|
||||||
grant_flows %w(authorization_code client_credentials)
|
grant_flows %w(authorization_code client_credentials refresh_token)
|
||||||
|
|
||||||
|
# If the client is not a confidential client, it should not be able to use the
|
||||||
|
# client_credentials grant flow, since it cannot keep a secret.
|
||||||
|
allow_grant_flow_for_client do |grant_flow, client|
|
||||||
|
if grant_flow == Doorkeeper::OAuth::CLIENT_CREDENTIALS
|
||||||
|
client.confidential?
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Under some circumstances you might want to have applications auto-approved,
|
# Under some circumstances you might want to have applications auto-approved,
|
||||||
# so that the user skips the authorization step.
|
# so that the user skips the authorization step.
|
||||||
|
|
|
@ -89,6 +89,7 @@ en:
|
||||||
invalid_request:
|
invalid_request:
|
||||||
missing_param: 'Missing required parameter: %{value}.'
|
missing_param: 'Missing required parameter: %{value}.'
|
||||||
request_not_authorized: Request need to be authorized. Required parameter for authorizing request is missing or invalid.
|
request_not_authorized: Request need to be authorized. Required parameter for authorizing request is missing or invalid.
|
||||||
|
offline_access_only: The offline_access scope can only be used with other scopes.
|
||||||
unknown: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
|
unknown: The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.
|
||||||
invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found
|
invalid_resource_owner: The provided resource owner credentials are not valid, or resource owner cannot be found
|
||||||
invalid_scope: The requested scope is invalid, unknown, or malformed.
|
invalid_scope: The requested scope is invalid, unknown, or malformed.
|
||||||
|
@ -118,6 +119,7 @@ en:
|
||||||
read: Read-only access
|
read: Read-only access
|
||||||
read/write: Read and write access
|
read/write: Read and write access
|
||||||
write: Write-only access
|
write: Write-only access
|
||||||
|
offline: Access for an extended period of time
|
||||||
title:
|
title:
|
||||||
accounts: Accounts
|
accounts: Accounts
|
||||||
admin/accounts: Administration of accounts
|
admin/accounts: Administration of accounts
|
||||||
|
@ -138,6 +140,7 @@ en:
|
||||||
notifications: Notifications
|
notifications: Notifications
|
||||||
profile: Your Mastodon profile
|
profile: Your Mastodon profile
|
||||||
push: Push notifications
|
push: Push notifications
|
||||||
|
offline_access: Offline access
|
||||||
reports: Reports
|
reports: Reports
|
||||||
search: Search
|
search: Search
|
||||||
statuses: Posts
|
statuses: Posts
|
||||||
|
|
|
@ -34,12 +34,19 @@ RSpec.describe OAuth::AuthorizationsController do
|
||||||
|
|
||||||
context 'when app is already authorized' do
|
context 'when app is already authorized' do
|
||||||
before do
|
before do
|
||||||
|
context = Doorkeeper::OAuth::Authorization::Token.build_context(
|
||||||
|
app,
|
||||||
|
Doorkeeper::OAuth::AUTHORIZATION_CODE,
|
||||||
|
app.scopes,
|
||||||
|
user.id
|
||||||
|
)
|
||||||
|
|
||||||
Doorkeeper::AccessToken.find_or_create_for(
|
Doorkeeper::AccessToken.find_or_create_for(
|
||||||
application: app,
|
application: context.client,
|
||||||
resource_owner: user.id,
|
resource_owner: context.resource_owner,
|
||||||
scopes: app.scopes,
|
scopes: context.scopes,
|
||||||
expires_in: Doorkeeper.configuration.access_token_expires_in,
|
expires_in: Doorkeeper::OAuth::Authorization::Token.access_token_expires_in(Doorkeeper.config, context),
|
||||||
use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
|
use_refresh_token: Doorkeeper::OAuth::Authorization::Token.refresh_token_enabled?(Doorkeeper.config, context)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
Fabricator(:application, from: Doorkeeper::Application) do
|
Fabricator(:application, from: Doorkeeper::Application) do
|
||||||
name 'Example'
|
name 'Example'
|
||||||
website 'http://example.com'
|
website 'http://example.com'
|
||||||
redirect_uri 'http://example.com/callback'
|
redirect_uri 'urn:ietf:wg:oauth:2.0:oob'
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,12 @@ RSpec.describe ScopeTransformer do
|
||||||
it_behaves_like 'a scope', nil, 'profile', 'read'
|
it_behaves_like 'a scope', nil, 'profile', 'read'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with scope "offline_access"' do
|
||||||
|
let(:input) { 'offline_access' }
|
||||||
|
|
||||||
|
it_behaves_like 'a scope', nil, 'offline_access', 'offline'
|
||||||
|
end
|
||||||
|
|
||||||
context 'with scope "read"' do
|
context 'with scope "read"' do
|
||||||
let(:input) { 'read' }
|
let(:input) { 'read' }
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@ RSpec.describe 'Apps' do
|
||||||
expect(app).to be_present
|
expect(app).to be_present
|
||||||
expect(app.scopes.to_s).to eq scopes
|
expect(app.scopes.to_s).to eq scopes
|
||||||
expect(app.redirect_uris).to eq redirect_uris
|
expect(app.redirect_uris).to eq redirect_uris
|
||||||
|
expect(app.confidential).to be true
|
||||||
|
|
||||||
expect(response.parsed_body).to match(
|
expect(response.parsed_body).to match(
|
||||||
a_hash_including(
|
a_hash_including(
|
||||||
|
@ -55,6 +56,76 @@ RSpec.describe 'Apps' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'without being a confidential application' do
|
||||||
|
let(:client_name) { 'Test confidential app' }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
client_name: client_name,
|
||||||
|
redirect_uris: redirect_uris,
|
||||||
|
scopes: scopes,
|
||||||
|
website: website,
|
||||||
|
token_endpoint_auth_method: 'none',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates an public OAuth app', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
|
||||||
|
app = Doorkeeper::Application.find_by(name: client_name)
|
||||||
|
|
||||||
|
expect(app).to be_present
|
||||||
|
expect(app.scopes.to_s).to eq scopes
|
||||||
|
expect(app.redirect_uris).to eq redirect_uris
|
||||||
|
expect(app.confidential).to be false
|
||||||
|
|
||||||
|
expect(response.parsed_body).to match(
|
||||||
|
a_hash_including(
|
||||||
|
id: app.id.to_s,
|
||||||
|
client_id: app.uid,
|
||||||
|
client_secret: nil,
|
||||||
|
client_secret_expires_at: 0,
|
||||||
|
name: client_name,
|
||||||
|
website: website,
|
||||||
|
scopes: ['read', 'write'],
|
||||||
|
redirect_uris: redirect_uris,
|
||||||
|
# Deprecated properties as of 4.3:
|
||||||
|
redirect_uri: redirect_uri,
|
||||||
|
vapid_key: Rails.configuration.x.vapid_public_key
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when token_endpoint_auth_method is unknown' do
|
||||||
|
let(:client_name) { 'Test unknown auth app' }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
client_name: client_name,
|
||||||
|
redirect_uris: redirect_uris,
|
||||||
|
scopes: scopes,
|
||||||
|
website: website,
|
||||||
|
# Not yet supported:
|
||||||
|
token_endpoint_auth_method: 'private_key_jwt',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create an OAuth app', :aggregate_failures do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(400)
|
||||||
|
expect(response.content_type)
|
||||||
|
.to start_with('application/json')
|
||||||
|
|
||||||
|
app = Doorkeeper::Application.find_by(name: client_name)
|
||||||
|
|
||||||
|
expect(app).to_not be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'without scopes being supplied' do
|
context 'without scopes being supplied' do
|
||||||
let(:scopes) { nil }
|
let(:scopes) { nil }
|
||||||
|
|
||||||
|
|
|
@ -5,17 +5,19 @@ require 'rails_helper'
|
||||||
RSpec.describe 'Managing OAuth Tokens' do
|
RSpec.describe 'Managing OAuth Tokens' do
|
||||||
describe 'POST /oauth/token' do
|
describe 'POST /oauth/token' do
|
||||||
subject do
|
subject do
|
||||||
post '/oauth/token', params: params
|
post '/oauth/token', params: params, headers: {
|
||||||
|
# This is using the OAuth client_secret_basic client authentication method
|
||||||
|
Authorization: ActionController::HttpAuthentication::Basic.encode_credentials(application.uid, application.secret),
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:application) do
|
let(:application) do
|
||||||
Fabricate(:application, scopes: 'read write follow', redirect_uri: 'urn:ietf:wg:oauth:2.0:oob')
|
Fabricate(:application, scopes: 'read write follow', redirect_uri: 'urn:ietf:wg:oauth:2.0:oob')
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
grant_type: grant_type,
|
grant_type: grant_type,
|
||||||
client_id: application.uid,
|
|
||||||
client_secret: application.secret,
|
|
||||||
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
||||||
code: code,
|
code: code,
|
||||||
scope: scope,
|
scope: scope,
|
||||||
|
@ -103,6 +105,53 @@ RSpec.describe 'Managing OAuth Tokens' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with grant_type 'refresh_token'" do
|
||||||
|
let(:grant_type) { 'refresh_token' }
|
||||||
|
|
||||||
|
let!(:user) { Fabricate(:user) }
|
||||||
|
let!(:application) { Fabricate(:application, scopes: 'read offline_access') }
|
||||||
|
let!(:access_token) do
|
||||||
|
Fabricate(
|
||||||
|
:accessible_access_token,
|
||||||
|
resource_owner_id: user.id,
|
||||||
|
application: application,
|
||||||
|
# Even though the `application` uses the `offline_access` scope, the
|
||||||
|
# generation of a refresh token only happens when the model is created
|
||||||
|
# with `use_refresh_token: true`.
|
||||||
|
#
|
||||||
|
# This is normally determined from the request to create the access
|
||||||
|
# token, but here we are just creating the access token model, so we
|
||||||
|
# need to force the `access_token` to have `use_refresh_token: true`
|
||||||
|
use_refresh_token: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let!(:web_push_subscription) { Fabricate(:web_push_subscription, user: user, access_token: access_token) }
|
||||||
|
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
grant_type: grant_type,
|
||||||
|
refresh_token: access_token.refresh_token,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the Web::PushSubscription when refreshed' do
|
||||||
|
expect { subject }
|
||||||
|
.to change { access_token.reload.revoked_at }.from(nil).to(be_present)
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
|
||||||
|
new_token = Doorkeeper::AccessToken.by_token(response.parsed_body[:access_token])
|
||||||
|
|
||||||
|
expect(web_push_subscription.reload.access_token_id).to eq(new_token.id)
|
||||||
|
|
||||||
|
# Assert that there are definitely no subscriptions left for the
|
||||||
|
# previous access token:
|
||||||
|
expect(Web::PushSubscription.where(access_token: access_token.id).count)
|
||||||
|
.to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /oauth/revoke' do
|
describe 'POST /oauth/revoke' do
|
||||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe 'The /.well-known/oauth-authorization-server request' do
|
||||||
scopes_supported: Doorkeeper.configuration.scopes.map(&:to_s),
|
scopes_supported: Doorkeeper.configuration.scopes.map(&:to_s),
|
||||||
response_types_supported: Doorkeeper.configuration.authorization_response_types,
|
response_types_supported: Doorkeeper.configuration.authorization_response_types,
|
||||||
response_modes_supported: Doorkeeper.configuration.authorization_response_flows.flat_map(&:response_mode_matches).uniq,
|
response_modes_supported: Doorkeeper.configuration.authorization_response_flows.flat_map(&:response_mode_matches).uniq,
|
||||||
token_endpoint_auth_methods_supported: %w(client_secret_basic client_secret_post),
|
token_endpoint_auth_methods_supported: %w(none client_secret_basic client_secret_post),
|
||||||
grant_types_supported: grant_types_supported,
|
grant_types_supported: grant_types_supported,
|
||||||
code_challenge_methods_supported: Doorkeeper.configuration.pkce_code_challenge_methods_supported,
|
code_challenge_methods_supported: Doorkeeper.configuration.pkce_code_challenge_methods_supported,
|
||||||
# non-standard extension:
|
# non-standard extension:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user